From fa56047a8248f14ce704fc304b4b68873def84e4 Mon Sep 17 00:00:00 2001 From: Siiiiinth <60766886+Siiiiinth@users.noreply.github.com> Date: Wed, 14 Jan 2026 22:47:31 +0100 Subject: [PATCH 01/28] wip ui --- backend/playbackengine.go | 2 ++ ui/widgets/auxcontrols.go | 22 +++++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/backend/playbackengine.go b/backend/playbackengine.go index 8db1ff0..d152bed 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -66,6 +66,7 @@ type playbackEngine struct { nowPlayingIdx int isRadio bool loopMode LoopMode + shuffle bool pauseAfterCurrent bool // flag to pause playback after current track ends @@ -98,6 +99,7 @@ type playbackEngine struct { onSongChange []func(nowPlaying mediaprovider.MediaItem, justScrobbledIfAny *mediaprovider.Track) onPlayTimeUpdate []func(float64, float64, bool) onLoopModeChange []func(LoopMode) + onShuffleChange []func() onVolumeChange []func(int) onSeek []func() onPaused []func() diff --git a/ui/widgets/auxcontrols.go b/ui/widgets/auxcontrols.go index 558ea72..3da8839 100644 --- a/ui/widgets/auxcontrols.go +++ b/ui/widgets/auxcontrols.go @@ -22,8 +22,10 @@ type AuxControls struct { widget.BaseWidget OnChangeAutoplay func(autoplay bool) + OnChangeShuffle func(shuffle bool) VolumeControl *VolumeControl + shuffle *IconButton autoplay *IconButton loop *IconButton cast *IconButton @@ -35,12 +37,22 @@ type AuxControls struct { func NewAuxControls(initialVolume int, initialLoopMode backend.LoopMode, initialAutoplay bool) *AuxControls { a := &AuxControls{ VolumeControl: NewVolumeControl(initialVolume), + shuffle: NewIconButton(myTheme.ShuffleIcon, nil), autoplay: NewIconButton(myTheme.AutoplayIcon, nil), loop: NewIconButton(myTheme.RepeatIcon, nil), cast: NewIconButton(myTheme.CastIcon, nil), showQueue: NewIconButton(myTheme.PlayQueueIcon, nil), } + a.shuffle.IconSize = IconButtonSizeSmaller + a.shuffle.SetToolTip(lang.L("Shuffle")) + a.shuffle.OnTapped = func() { + a.SetShuffle(!a.shuffle.Highlighted) + if a.OnChangeShuffle != nil { + a.OnChangeShuffle(a.shuffle.Highlighted) + } + } + a.loop.IconSize = IconButtonSizeSmaller a.loop.SetToolTip(lang.L("Repeat")) a.SetLoopMode(initialLoopMode) @@ -68,7 +80,7 @@ func NewAuxControls(initialVolume int, initialLoopMode backend.LoopMode, initial a.VolumeControl, container.New( layout.NewCustomPaddedHBoxLayout(theme.Padding()*1.5), - layout.NewSpacer(), a.autoplay, a.loop, a.cast, a.showQueue, util.NewHSpace(5)), + layout.NewSpacer(), a.autoplay, a.shuffle, a.loop, a.cast, a.showQueue, util.NewHSpace(5)), layout.NewSpacer(), ), ) @@ -98,6 +110,14 @@ func (a *AuxControls) SetLoopMode(mode backend.LoopMode) { } } +func (a *AuxControls) SetShuffle(isShuffle bool) { + if isShuffle { + a.shuffle.Highlighted = true + } else { + a.shuffle.Highlighted = false + } +} + func (a *AuxControls) DisableCastButton() { a.cast.Disable() } From 405d9643b348f3a348765430b4a70ee49162f009 Mon Sep 17 00:00:00 2001 From: Siiiiinth <60766886+Siiiiinth@users.noreply.github.com> Date: Sat, 17 Jan 2026 19:03:33 +0100 Subject: [PATCH 02/28] added shuffle button to UI + base data flow --- backend/playbackengine.go | 9 +++++++++ backend/playbackmanager.go | 8 ++++++++ ui/bottompanel.go | 3 +++ ui/widgets/auxcontrols.go | 8 ++++---- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/backend/playbackengine.go b/backend/playbackengine.go index d152bed..6509d92 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -263,6 +263,15 @@ func (p *playbackEngine) GetLoopMode() LoopMode { return p.loopMode } +func (p *playbackEngine) SetShuffle(shuffle bool) { + p.shuffle = shuffle + //TODO: Implement functionality +} + +func (p *playbackEngine) GetShuffle() bool { + return p.shuffle +} + func (p *playbackEngine) PlaybackStatus() PlaybackStatus { stat := p.pendingPlayerChangeStatus if !p.pendingPlayerChange { diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index 8b9e361..ac160d8 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -373,6 +373,10 @@ func (p *PlaybackManager) LoadAlbum(albumID string, insertQueueMode InsertQueueM return nil } +func (p *PlaybackManager) GetShuffle() bool { + return p.engine.GetShuffle() +} + // Loads the specified playlist into the play queue. func (p *PlaybackManager) LoadPlaylist(playlistID string, insertQueueMode InsertQueueMode, shuffle bool) error { playlist, err := p.engine.sm.Server.GetPlaylist(playlistID) @@ -639,6 +643,10 @@ func (p *PlaybackManager) SetAutoplay(autoplay bool) { } } +func (p *PlaybackManager) SetShuffle(shuffle bool) { + p.engine.SetShuffle(shuffle) +} + func (p *PlaybackManager) Volume() int { return p.engine.CurrentPlayer().GetVolume() } diff --git a/ui/bottompanel.go b/ui/bottompanel.go index ba98b93..c4afe87 100644 --- a/ui/bottompanel.go +++ b/ui/bottompanel.go @@ -122,6 +122,9 @@ func NewBottomPanel(pm *backend.PlaybackManager, im *backend.ImageManager, contr bp.AuxControls.OnChangeAutoplay = func(autoplay bool) { pm.SetAutoplay(autoplay) } + bp.AuxControls.OnChangeShuffle = func(shuffle bool) { + pm.SetShuffle(shuffle) + } bp.AuxControls.OnShowPlayQueue(contr.ShowPopUpPlayQueue) bp.AuxControls.OnShowCastMenu(contr.ShowCastMenu) diff --git a/ui/widgets/auxcontrols.go b/ui/widgets/auxcontrols.go index 3da8839..b77b140 100644 --- a/ui/widgets/auxcontrols.go +++ b/ui/widgets/auxcontrols.go @@ -111,11 +111,11 @@ func (a *AuxControls) SetLoopMode(mode backend.LoopMode) { } func (a *AuxControls) SetShuffle(isShuffle bool) { - if isShuffle { - a.shuffle.Highlighted = true - } else { - a.shuffle.Highlighted = false + if isShuffle == a.shuffle.Highlighted { + return } + a.shuffle.Highlighted = isShuffle + a.shuffle.Refresh() } func (a *AuxControls) DisableCastButton() { From 169304b9311ec719625f61f15306308dea7f133a Mon Sep 17 00:00:00 2001 From: Siiiiinth <60766886+Siiiiinth@users.noreply.github.com> Date: Sat, 17 Jan 2026 19:11:02 +0100 Subject: [PATCH 03/28] rem callback --- backend/playbackengine.go | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/playbackengine.go b/backend/playbackengine.go index 6509d92..43127a0 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -99,7 +99,6 @@ type playbackEngine struct { onSongChange []func(nowPlaying mediaprovider.MediaItem, justScrobbledIfAny *mediaprovider.Track) onPlayTimeUpdate []func(float64, float64, bool) onLoopModeChange []func(LoopMode) - onShuffleChange []func() onVolumeChange []func(int) onSeek []func() onPaused []func() From bab54833c3fd5dd737c177b263a7134f50b3208e Mon Sep 17 00:00:00 2001 From: Siiiiinth <60766886+Siiiiinth@users.noreply.github.com> Date: Sat, 24 Jan 2026 21:18:21 +0100 Subject: [PATCH 04/28] wip --- backend/playbackengine.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/backend/playbackengine.go b/backend/playbackengine.go index 43127a0..7ebcd7a 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -62,11 +62,12 @@ type playbackEngine struct { latestTrackPosition float64 // cleared by checkScrobble callbacksDisabled bool - playQueue []mediaprovider.MediaItem - nowPlayingIdx int - isRadio bool - loopMode LoopMode - shuffle bool + playQueue []mediaprovider.MediaItem + playQueueShuffle []mediaprovider.MediaItem + nowPlayingIdx int + isRadio bool + loopMode LoopMode + shuffle bool pauseAfterCurrent bool // flag to pause playback after current track ends @@ -217,6 +218,8 @@ func (p *playbackEngine) SetPlayer(pl player.BasePlayer) error { return nil } +// Interface functions for interacting with the play queue + func (p *playbackEngine) PlayTrackAt(idx int) error { return p.playTrackAt(idx, 0) } @@ -264,7 +267,7 @@ func (p *playbackEngine) GetLoopMode() LoopMode { func (p *playbackEngine) SetShuffle(shuffle bool) { p.shuffle = shuffle - //TODO: Implement functionality + //TODO_SHUFFLE: Implement functionality } func (p *playbackEngine) GetShuffle() bool { From ac480ec92ca9ec79f4a550038cd4efa27714b805 Mon Sep 17 00:00:00 2001 From: Siiiiinth <60766886+Siiiiinth@users.noreply.github.com> Date: Sat, 24 Jan 2026 21:39:46 +0100 Subject: [PATCH 05/28] getPlayQueueLength func --- backend/playbackengine.go | 45 ++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/backend/playbackengine.go b/backend/playbackengine.go index 7ebcd7a..825f2e5 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -64,10 +64,11 @@ type playbackEngine struct { playQueue []mediaprovider.MediaItem playQueueShuffle []mediaprovider.MediaItem - nowPlayingIdx int - isRadio bool - loopMode LoopMode - shuffle bool + + nowPlayingIdx int + isRadio bool + loopMode LoopMode + shuffle bool pauseAfterCurrent bool // flag to pause playback after current track ends @@ -220,12 +221,16 @@ func (p *playbackEngine) SetPlayer(pl player.BasePlayer) error { // Interface functions for interacting with the play queue +func (p *playbackEngine) getPlayQueueLength() int { + return len(p.playQueue) +} + func (p *playbackEngine) PlayTrackAt(idx int) error { return p.playTrackAt(idx, 0) } func (p *playbackEngine) playTrackAt(idx int, startTime float64) error { - if l := len(p.playQueue); idx < 0 || idx >= l { + if l := p.getPlayQueueLength(); idx < 0 || idx >= l { return fmt.Errorf("track index (%d) out of range (0-%d)", idx, l) } // scrobble current track if needed @@ -238,7 +243,7 @@ func (p *playbackEngine) playTrackAt(idx int, startTime float64) error { // Gets the curently playing media item, if any. func (p *playbackEngine) NowPlaying() mediaprovider.MediaItem { - if p.nowPlayingIdx < 0 || len(p.playQueue) == 0 || p.player.GetStatus().State == player.Stopped { + if p.nowPlayingIdx < 0 || p.getPlayQueueLength() == 0 || p.player.GetStatus().State == player.Stopped { return nil } return p.playQueue[p.nowPlayingIdx] @@ -324,7 +329,7 @@ func (p *playbackEngine) SeekFwdBackN(n int) error { return p.player.SeekSeconds(0) // seek back in current song } - lastIdx := len(p.playQueue) - 1 + lastIdx := p.getPlayQueueLength() - 1 newIdx := min(lastIdx, max(0, idx+n)) if idx == lastIdx && n > 0 { @@ -389,7 +394,7 @@ func (p *playbackEngine) doLoaditems(items []mediaprovider.MediaItem, insertQueu p.nowPlayingIdx = -1 p.playQueue = nil } - if nextChanged := len(items) > 0 && (insertQueueMode != Append || (p.nowPlayingIdx == len(p.playQueue)-1)); nextChanged { + if nextChanged := len(items) > 0 && (insertQueueMode != Append || (p.nowPlayingIdx == p.getPlayQueueLength()-1)); nextChanged { defer p.handleNextTrackUpdated() } @@ -397,7 +402,7 @@ func (p *playbackEngine) doLoaditems(items []mediaprovider.MediaItem, insertQueu rand.Shuffle(len(items), func(i, j int) { items[i], items[j] = items[j], items[i] }) } - insertIdx := len(p.playQueue) + insertIdx := p.getPlayQueueLength() if insertQueueMode == InsertNext { insertIdx = p.nowPlayingIdx + 1 } @@ -413,14 +418,14 @@ func (p *playbackEngine) LoadRadioStation(radio *mediaprovider.RadioStation, ins p.nowPlayingIdx = -1 p.playQueue = nil } - if nextChanged := insertMode == InsertNext || (insertMode == Append && p.nowPlayingIdx == len(p.playQueue)-1); nextChanged { + if nextChanged := insertMode == InsertNext || (insertMode == Append && p.nowPlayingIdx == p.getPlayQueueLength()-1); nextChanged { p.handleNextTrackUpdated() } - insertIdx := len(p.playQueue) + insertIdx := p.getPlayQueueLength() if insertMode == InsertNext { insertIdx = p.nowPlayingIdx + 1 } - new := make([]mediaprovider.MediaItem, len(p.playQueue)+1) + new := make([]mediaprovider.MediaItem, p.getPlayQueueLength()+1) firstHalf := p.playQueue[:insertIdx] copy(new, firstHalf) new[len(firstHalf)] = radio @@ -432,7 +437,7 @@ func (p *playbackEngine) LoadRadioStation(radio *mediaprovider.RadioStation, ins // Stop playback and clear the play queue. func (p *playbackEngine) StopAndClearPlayQueue() { - changed := len(p.playQueue) > 0 + changed := p.getPlayQueueLength() > 0 p.player.Stop(false) p.playQueue = nil p.nowPlayingIdx = -1 @@ -495,7 +500,7 @@ func (p *playbackEngine) UpdatePlayQueue(items []mediaprovider.MediaItem) error } func (p *playbackEngine) RemoveTracksFromQueue(idxs []int) { - newQueue := make([]mediaprovider.MediaItem, 0, len(p.playQueue)-len(idxs)) + newQueue := make([]mediaprovider.MediaItem, 0, p.getPlayQueueLength()-len(idxs)) idxSet := sharedutil.ToSet(idxs) isPlayingTrackRemoved := false isNextPlayingTrackremoved := false @@ -590,7 +595,7 @@ func (p *playbackEngine) cacheNextTracks() { // the "currently" playing track, since we're probably about to play it npI := max(p.nowPlayingIdx, 0) for _, idx := range [3]int{npI, npI + 1, npI + 2} { - if idx > 0 && idx < len(p.playQueue) { + if idx > 0 && idx < p.getPlayQueueLength() { item := p.playQueue[idx] if item.Metadata().Type == mediaprovider.MediaItemTypeTrack { fetch = append(fetch, AudioCacheRequest{ @@ -619,7 +624,7 @@ func (p *playbackEngine) handleOnTrackChange() { } if p.pendingTrackChangeNum < 0 && (p.wasStopped || p.loopMode != LoopOne) { p.nowPlayingIdx++ - if p.loopMode == LoopAll && p.nowPlayingIdx == len(p.playQueue) { + if p.loopMode == LoopAll && p.nowPlayingIdx == p.getPlayQueueLength() { p.nowPlayingIdx = 0 // wrapped around } } else if p.pendingTrackChangeNum >= 0 { @@ -678,14 +683,14 @@ func (p *playbackEngine) handleNextTrackUpdated() { func (p *playbackEngine) nextPlayingIndex() int { switch p.loopMode { case LoopNone: - if p.nowPlayingIdx >= len(p.playQueue)-1 { + if p.nowPlayingIdx >= p.getPlayQueueLength()-1 { return -1 } return p.nowPlayingIdx + 1 case LoopOne: return p.nowPlayingIdx case LoopAll: - if p.nowPlayingIdx >= len(p.playQueue)-1 { + if p.nowPlayingIdx >= p.getPlayQueueLength()-1 { return 0 } return p.nowPlayingIdx + 1 @@ -778,7 +783,7 @@ func (p *playbackEngine) setNextTrack(idx int) error { // call BEFORE updating p.nowPlayingIdx func (p *playbackEngine) checkScrobble() { - if !p.scrobbleCfg.Enabled || len(p.playQueue) == 0 || p.nowPlayingIdx < 0 { + if !p.scrobbleCfg.Enabled || p.getPlayQueueLength() == 0 || p.nowPlayingIdx < 0 { return } track, ok := p.playQueue[p.nowPlayingIdx].(*mediaprovider.Track) @@ -807,7 +812,7 @@ func (p *playbackEngine) checkScrobble() { } func (p *playbackEngine) sendNowPlayingScrobble() { - if !p.scrobbleCfg.Enabled || len(p.playQueue) == 0 || p.nowPlayingIdx < 0 { + if !p.scrobbleCfg.Enabled || p.getPlayQueueLength() == 0 || p.nowPlayingIdx < 0 { return } track, ok := p.playQueue[p.nowPlayingIdx].(*mediaprovider.Track) From 83edeafe7cb3ad348a70fafe59677d37fb0791e9 Mon Sep 17 00:00:00 2001 From: Siiiiinth <60766886+Siiiiinth@users.noreply.github.com> Date: Sat, 24 Jan 2026 21:43:29 +0100 Subject: [PATCH 06/28] clearPlayQueue func --- backend/playbackengine.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/backend/playbackengine.go b/backend/playbackengine.go index 825f2e5..27476ac 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -222,9 +222,17 @@ func (p *playbackEngine) SetPlayer(pl player.BasePlayer) error { // Interface functions for interacting with the play queue func (p *playbackEngine) getPlayQueueLength() int { + if p.shuffle { + return len(p.playQueueShuffle) + } return len(p.playQueue) } +func (p *playbackEngine) clearPlayQueue() { + p.playQueue = nil + p.playQueueShuffle = nil +} + func (p *playbackEngine) PlayTrackAt(idx int) error { return p.playTrackAt(idx, 0) } @@ -392,7 +400,7 @@ func (p *playbackEngine) doLoaditems(items []mediaprovider.MediaItem, insertQueu if insertQueueMode == Replace { p.player.Stop(false) p.nowPlayingIdx = -1 - p.playQueue = nil + p.clearPlayQueue() } if nextChanged := len(items) > 0 && (insertQueueMode != Append || (p.nowPlayingIdx == p.getPlayQueueLength()-1)); nextChanged { defer p.handleNextTrackUpdated() @@ -416,7 +424,7 @@ func (p *playbackEngine) LoadRadioStation(radio *mediaprovider.RadioStation, ins if insertMode == Replace { p.player.Stop(false) p.nowPlayingIdx = -1 - p.playQueue = nil + p.clearPlayQueue() } if nextChanged := insertMode == InsertNext || (insertMode == Append && p.nowPlayingIdx == p.getPlayQueueLength()-1); nextChanged { p.handleNextTrackUpdated() @@ -439,7 +447,7 @@ func (p *playbackEngine) LoadRadioStation(radio *mediaprovider.RadioStation, ins func (p *playbackEngine) StopAndClearPlayQueue() { changed := p.getPlayQueueLength() > 0 p.player.Stop(false) - p.playQueue = nil + p.clearPlayQueue() p.nowPlayingIdx = -1 if changed { p.invokeNoArgCallbacks(p.onQueueChange) From 72f2068dc38e9696dac4fd6dfc6f620e434a4231 Mon Sep 17 00:00:00 2001 From: Siiiiinth <60766886+Siiiiinth@users.noreply.github.com> Date: Sat, 24 Jan 2026 21:47:33 +0100 Subject: [PATCH 07/28] setPlayQueue func --- backend/playbackengine.go | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/backend/playbackengine.go b/backend/playbackengine.go index 27476ac..992c0df 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -233,6 +233,14 @@ func (p *playbackEngine) clearPlayQueue() { p.playQueueShuffle = nil } +func (p *playbackEngine) setPlayQueue(items []mediaprovider.MediaItem) { + if p.shuffle { + p.playQueueShuffle = items + } else { + p.playQueue = items + } +} + func (p *playbackEngine) PlayTrackAt(idx int) error { return p.playTrackAt(idx, 0) } @@ -414,7 +422,7 @@ func (p *playbackEngine) doLoaditems(items []mediaprovider.MediaItem, insertQueu if insertQueueMode == InsertNext { insertIdx = p.nowPlayingIdx + 1 } - p.playQueue = append(p.playQueue[:insertIdx], append(items, p.playQueue[insertIdx:]...)...) + p.setPlayQueue(append(p.playQueue[:insertIdx], append(items, p.playQueue[insertIdx:]...)...)) p.invokeNoArgCallbacks(p.onQueueChange) return nil @@ -438,7 +446,7 @@ func (p *playbackEngine) LoadRadioStation(radio *mediaprovider.RadioStation, ins copy(new, firstHalf) new[len(firstHalf)] = radio copy(new[len(firstHalf)+1:], p.playQueue[insertIdx:]) - p.playQueue = new + p.setPlayQueue(new) p.invokeNoArgCallbacks(p.onQueueChange) } @@ -494,7 +502,7 @@ func (p *playbackEngine) UpdatePlayQueue(items []mediaprovider.MediaItem) error } } - p.playQueue = newQueue + p.setPlayQueue(newQueue) if p.nowPlayingIdx >= 0 && newNowPlayingIdx == -1 { return p.Stop() } @@ -533,7 +541,7 @@ func (p *playbackEngine) RemoveTracksFromQueue(idxs []int) { newQueue = append(newQueue, tr) } } - p.playQueue = newQueue + p.setPlayQueue(newQueue) p.nowPlayingIdx = newNowPlaying if isPlayingTrackRemoved { if newNowPlaying == len(newQueue) { From 65081246d9cd185e70dbbb0e13eb5c3b52597d9a Mon Sep 17 00:00:00 2001 From: Siiiiinth <60766886+Siiiiinth@users.noreply.github.com> Date: Sat, 24 Jan 2026 22:01:24 +0100 Subject: [PATCH 08/28] reordered playqueue func --- backend/playbackengine.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/backend/playbackengine.go b/backend/playbackengine.go index 992c0df..d7417ec 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -241,6 +241,11 @@ func (p *playbackEngine) setPlayQueue(items []mediaprovider.MediaItem) { } } +// rename to GetPlayQueueExternal +func (p *playbackEngine) GetPlayQueue() []mediaprovider.MediaItem { + return deepCopyMediaItemSlice(p.playQueue) +} + func (p *playbackEngine) PlayTrackAt(idx int) error { return p.playTrackAt(idx, 0) } @@ -462,10 +467,6 @@ func (p *playbackEngine) StopAndClearPlayQueue() { } } -func (p *playbackEngine) GetPlayQueue() []mediaprovider.MediaItem { - return deepCopyMediaItemSlice(p.playQueue) -} - // 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) { From 79b2b293fc63894db68943aed239ea97896fa845 Mon Sep 17 00:00:00 2001 From: Siiiiinth <60766886+Siiiiinth@users.noreply.github.com> Date: Sat, 24 Jan 2026 22:46:58 +0100 Subject: [PATCH 09/28] getPlayQueueItemAt func --- backend/playbackengine.go | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/backend/playbackengine.go b/backend/playbackengine.go index d7417ec..4c81fec 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -241,6 +241,10 @@ func (p *playbackEngine) setPlayQueue(items []mediaprovider.MediaItem) { } } +func (p *playbackEngine) getPlayQueueItemAt(idx int) mediaprovider.MediaItem { + return p.playQueue[idx] +} + // rename to GetPlayQueueExternal func (p *playbackEngine) GetPlayQueue() []mediaprovider.MediaItem { return deepCopyMediaItemSlice(p.playQueue) @@ -267,7 +271,7 @@ func (p *playbackEngine) NowPlaying() mediaprovider.MediaItem { if p.nowPlayingIdx < 0 || p.getPlayQueueLength() == 0 || p.player.GetStatus().State == player.Stopped { return nil } - return p.playQueue[p.nowPlayingIdx] + return p.getPlayQueueItemAt(p.nowPlayingIdx) } func (p *playbackEngine) NowPlayingIndex() int { @@ -494,7 +498,7 @@ func (p *playbackEngine) UpdatePlayQueue(items []mediaprovider.MediaItem) error newQueue := deepCopyMediaItemSlice(items) newNowPlayingIdx := -1 if p.nowPlayingIdx >= 0 { - nowPlayingID := p.playQueue[p.nowPlayingIdx].Metadata().ID + nowPlayingID := p.getPlayQueueItemAt(p.nowPlayingIdx).Metadata().ID for i, tr := range newQueue { if tr.Metadata().ID == nowPlayingID { newNowPlayingIdx = i @@ -613,10 +617,10 @@ func (p *playbackEngine) cacheNextTracks() { npI := max(p.nowPlayingIdx, 0) for _, idx := range [3]int{npI, npI + 1, npI + 2} { if idx > 0 && idx < p.getPlayQueueLength() { - item := p.playQueue[idx] + item := p.getPlayQueueItemAt(idx) if item.Metadata().Type == mediaprovider.MediaItemTypeTrack { fetch = append(fetch, AudioCacheRequest{ - ID: p.playQueue[idx].Metadata().ID, + ID: p.getPlayQueueItemAt(idx).Metadata().ID, DownloadURL: p.getMediaURLForIdx(idx), }) } @@ -648,7 +652,7 @@ func (p *playbackEngine) handleOnTrackChange() { p.nowPlayingIdx = p.pendingTrackChangeNum p.pendingTrackChangeNum = -1 } - nowPlaying := p.playQueue[p.nowPlayingIdx] + nowPlaying := p.getPlayQueueItemAt(p.nowPlayingIdx) _, isRadio := nowPlaying.(*mediaprovider.RadioStation) p.isRadio = isRadio @@ -691,7 +695,7 @@ func (p *playbackEngine) handleNextTrackUpdated() { for _, cb := range p.onBeforeSongChange { var item mediaprovider.MediaItem if idx := p.nextPlayingIndex(); idx >= 0 { - item = p.playQueue[idx] + item = p.getPlayQueueItemAt(idx) } cb(item) } @@ -719,7 +723,7 @@ func (p *playbackEngine) setTrack(idx int, next bool, startTime float64) error { var item mediaprovider.MediaItem var url string if idx >= 0 { - item = p.playQueue[idx] + item = p.getPlayQueueItemAt(idx) url = p.getMediaURLForIdx(idx) } track, isTrack := item.(*mediaprovider.Track) @@ -763,7 +767,7 @@ func (p *playbackEngine) setTrack(idx int, next bool, startTime float64) error { } else if trP, ok := p.player.(player.TrackPlayer); ok { var track *mediaprovider.Track if idx >= 0 { - track, ok = p.playQueue[idx].(*mediaprovider.Track) + track, ok = p.getPlayQueueItemAt(idx).(*mediaprovider.Track) if !ok { return errors.New("cannot play non-Track media item with TrackPlayer") } @@ -778,7 +782,7 @@ func (p *playbackEngine) setTrack(idx int, next bool, startTime float64) error { func (p *playbackEngine) getMediaURLForIdx(idx int) string { var url string - item := p.playQueue[idx] + item := p.getPlayQueueItemAt(idx) if tr, ok := item.(*mediaprovider.Track); ok { var ts *mediaprovider.TranscodeSettings if p.transcodeCfg.RequestTranscode { @@ -803,7 +807,7 @@ func (p *playbackEngine) checkScrobble() { if !p.scrobbleCfg.Enabled || p.getPlayQueueLength() == 0 || p.nowPlayingIdx < 0 { return } - track, ok := p.playQueue[p.nowPlayingIdx].(*mediaprovider.Track) + track, ok := p.getPlayQueueItemAt(p.nowPlayingIdx).(*mediaprovider.Track) if !ok { return // radio stations are not scrobbled } @@ -832,7 +836,7 @@ func (p *playbackEngine) sendNowPlayingScrobble() { if !p.scrobbleCfg.Enabled || p.getPlayQueueLength() == 0 || p.nowPlayingIdx < 0 { return } - track, ok := p.playQueue[p.nowPlayingIdx].(*mediaprovider.Track) + track, ok := p.getPlayQueueItemAt(p.nowPlayingIdx).(*mediaprovider.Track) if !ok { return // radio stations are not scrobbled } From ba0fcfb9ab60469bfbd5389bce36867094848293 Mon Sep 17 00:00:00 2001 From: Siiiiinth <60766886+Siiiiinth@users.noreply.github.com> Date: Sat, 24 Jan 2026 23:32:02 +0100 Subject: [PATCH 10/28] getPlayQueue func --- backend/playbackengine.go | 13 ++++++++----- backend/playbackmanager.go | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/backend/playbackengine.go b/backend/playbackengine.go index 4c81fec..02561e9 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -245,8 +245,11 @@ func (p *playbackEngine) getPlayQueueItemAt(idx int) mediaprovider.MediaItem { return p.playQueue[idx] } -// rename to GetPlayQueueExternal -func (p *playbackEngine) GetPlayQueue() []mediaprovider.MediaItem { +func (p *playbackEngine) getPlayQueue() []mediaprovider.MediaItem { + return p.playQueue +} + +func (p *playbackEngine) GetPlayQueueDeepCopy() []mediaprovider.MediaItem { return deepCopyMediaItemSlice(p.playQueue) } @@ -474,7 +477,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.playQueue); item != nil { + if item := sharedutil.FindMediaItemByID(id, p.getPlayQueue()); item != nil { if tr, ok := item.(*mediaprovider.Track); ok { tr.Favorite = fav } @@ -484,7 +487,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.playQueue); item != nil { + if item := sharedutil.FindMediaItemByID(id, p.getPlayQueue()); item != nil { if tr, ok := item.(*mediaprovider.Track); ok { tr.Rating = rating } @@ -527,7 +530,7 @@ func (p *playbackEngine) RemoveTracksFromQueue(idxs []int) { isNextPlayingTrackremoved := false nowPlaying := p.NowPlayingIndex() newNowPlaying := nowPlaying - for i, tr := range p.playQueue { + for i, tr := range p.getPlayQueue() { 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 ac160d8..9bf024a 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -571,7 +571,7 @@ func (p *PlaybackManager) fetchAndPlayTracks(fetchFn func() ([]*mediaprovider.Tr } func (p *PlaybackManager) GetPlayQueue() []mediaprovider.MediaItem { - return p.engine.GetPlayQueue() + return p.engine.GetPlayQueueDeepCopy() } // Any time the user changes the favorite status of a track elsewhere in the app, From d8b63d93d8c386fbea60901e0beaf2f4edc7fc0d Mon Sep 17 00:00:00 2001 From: Siiiiinth <60766886+Siiiiinth@users.noreply.github.com> Date: Sun, 25 Jan 2026 10:21:57 +0100 Subject: [PATCH 11/28] insertItemsIntoPlayQueueAt func --- backend/playbackengine.go | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/backend/playbackengine.go b/backend/playbackengine.go index 02561e9..21a4d3e 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -62,8 +62,8 @@ type playbackEngine struct { latestTrackPosition float64 // cleared by checkScrobble callbacksDisabled bool - playQueue []mediaprovider.MediaItem - playQueueShuffle []mediaprovider.MediaItem + playQueue []mediaprovider.MediaItem + shuffledPlayQueue []mediaprovider.MediaItem nowPlayingIdx int isRadio bool @@ -219,23 +219,23 @@ func (p *playbackEngine) SetPlayer(pl player.BasePlayer) error { return nil } -// Interface functions for interacting with the play queue +// ======================= START PLAY QUEUE FUNCS =========================== func (p *playbackEngine) getPlayQueueLength() int { if p.shuffle { - return len(p.playQueueShuffle) + return len(p.shuffledPlayQueue) } return len(p.playQueue) } func (p *playbackEngine) clearPlayQueue() { p.playQueue = nil - p.playQueueShuffle = nil + p.shuffledPlayQueue = nil } func (p *playbackEngine) setPlayQueue(items []mediaprovider.MediaItem) { if p.shuffle { - p.playQueueShuffle = items + p.shuffledPlayQueue = items } else { p.playQueue = items } @@ -245,6 +245,10 @@ func (p *playbackEngine) getPlayQueueItemAt(idx int) mediaprovider.MediaItem { return p.playQueue[idx] } +func (p *playbackEngine) insertItemsIntoPlayQueueAt(items []mediaprovider.MediaItem, idx int) { + p.setPlayQueue(append(p.playQueue[:idx], append(items, p.playQueue[idx:]...)...)) +} + func (p *playbackEngine) getPlayQueue() []mediaprovider.MediaItem { return p.playQueue } @@ -253,6 +257,8 @@ func (p *playbackEngine) GetPlayQueueDeepCopy() []mediaprovider.MediaItem { return deepCopyMediaItemSlice(p.playQueue) } +// ======================== END PLAY QUEUE FUNCS ============================= + func (p *playbackEngine) PlayTrackAt(idx int) error { return p.playTrackAt(idx, 0) } @@ -434,8 +440,8 @@ func (p *playbackEngine) doLoaditems(items []mediaprovider.MediaItem, insertQueu if insertQueueMode == InsertNext { insertIdx = p.nowPlayingIdx + 1 } - p.setPlayQueue(append(p.playQueue[:insertIdx], append(items, p.playQueue[insertIdx:]...)...)) + p.insertItemsIntoPlayQueueAt(items, insertIdx) p.invokeNoArgCallbacks(p.onQueueChange) return nil } @@ -457,7 +463,7 @@ func (p *playbackEngine) LoadRadioStation(radio *mediaprovider.RadioStation, ins firstHalf := p.playQueue[:insertIdx] copy(new, firstHalf) new[len(firstHalf)] = radio - copy(new[len(firstHalf)+1:], p.playQueue[insertIdx:]) + copy(new[len(firstHalf)+1:], p.playQueue[insertIdx:]) //TODO_SHUFFLE: Test shuffle with radio stations p.setPlayQueue(new) p.invokeNoArgCallbacks(p.onQueueChange) From 452ac8ff5b34dde2de109f26114ed7c60699ae51 Mon Sep 17 00:00:00 2001 From: Siiiiinth <60766886+Siiiiinth@users.noreply.github.com> Date: Sun, 25 Jan 2026 17:00:15 +0100 Subject: [PATCH 12/28] checkpoint shuffle impl --- backend/playbackengine.go | 44 ++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/backend/playbackengine.go b/backend/playbackengine.go index 21a4d3e..983d331 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -221,40 +221,37 @@ func (p *playbackEngine) SetPlayer(pl player.BasePlayer) error { // ======================= START PLAY QUEUE FUNCS =========================== -func (p *playbackEngine) getPlayQueueLength() int { +func (p *playbackEngine) getPlayQueue() []mediaprovider.MediaItem { if p.shuffle { - return len(p.shuffledPlayQueue) + return p.shuffledPlayQueue } - return len(p.playQueue) + return p.playQueue +} + +func (p *playbackEngine) getPlayQueueLength() int { + return len(p.getPlayQueue()) } func (p *playbackEngine) clearPlayQueue() { p.playQueue = nil - p.shuffledPlayQueue = nil + p.SetShuffle(false) } func (p *playbackEngine) setPlayQueue(items []mediaprovider.MediaItem) { - if p.shuffle { - p.shuffledPlayQueue = items - } else { - p.playQueue = items - } + p.playQueue = items } func (p *playbackEngine) getPlayQueueItemAt(idx int) mediaprovider.MediaItem { - return p.playQueue[idx] + return p.getPlayQueue()[idx] } func (p *playbackEngine) insertItemsIntoPlayQueueAt(items []mediaprovider.MediaItem, idx int) { - p.setPlayQueue(append(p.playQueue[:idx], append(items, p.playQueue[idx:]...)...)) -} - -func (p *playbackEngine) getPlayQueue() []mediaprovider.MediaItem { - return p.playQueue + p.playQueue = append(p.playQueue[:idx], append(items, p.playQueue[idx:]...)...) + p.shuffledPlayQueue = append(p.shuffledPlayQueue[:idx], append(items, p.shuffledPlayQueue[idx:]...)...) } func (p *playbackEngine) GetPlayQueueDeepCopy() []mediaprovider.MediaItem { - return deepCopyMediaItemSlice(p.playQueue) + return deepCopyMediaItemSlice(p.playQueue) // TODO_SHUFFLE: does this also need the shuffled play queue? } // ======================== END PLAY QUEUE FUNCS ============================= @@ -305,8 +302,21 @@ func (p *playbackEngine) GetLoopMode() LoopMode { } func (p *playbackEngine) SetShuffle(shuffle bool) { + if p.shuffle == shuffle { + return + } + if shuffle { + fmt.Println("shuffling") + items := deepCopyMediaItemSlice(p.playQueue) + rand.Shuffle(len(items), func(i, j int) { + items[i], items[j] = items[j], items[i] + }) + p.UpdatePlayQueue(items) + } else { + fmt.Println("deshuffling") + p.UpdatePlayQueue(p.playQueue) + } p.shuffle = shuffle - //TODO_SHUFFLE: Implement functionality } func (p *playbackEngine) GetShuffle() bool { From 177c43655d9ed2481a9abf2ff8b02b5da03c984b Mon Sep 17 00:00:00 2001 From: Siiiiinth <60766886+Siiiiinth@users.noreply.github.com> Date: Sun, 25 Jan 2026 18:20:35 +0100 Subject: [PATCH 13/28] shuffle func checkpoint 2 --- backend/playbackengine.go | 61 ++++++++++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 7 deletions(-) diff --git a/backend/playbackengine.go b/backend/playbackengine.go index 983d331..0601ffa 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -238,7 +238,11 @@ func (p *playbackEngine) clearPlayQueue() { } func (p *playbackEngine) setPlayQueue(items []mediaprovider.MediaItem) { - p.playQueue = items + if p.shuffle { + p.shuffledPlayQueue = items + } else { + p.playQueue = items + } } func (p *playbackEngine) getPlayQueueItemAt(idx int) mediaprovider.MediaItem { @@ -307,16 +311,59 @@ func (p *playbackEngine) SetShuffle(shuffle bool) { } if shuffle { fmt.Println("shuffling") - items := deepCopyMediaItemSlice(p.playQueue) - rand.Shuffle(len(items), func(i, j int) { - items[i], items[j] = items[j], items[i] + newQueue := deepCopyMediaItemSlice(p.playQueue) + rand.Shuffle(len(newQueue), func(i, j int) { + newQueue[i], newQueue[j] = newQueue[j], newQueue[i] }) - p.UpdatePlayQueue(items) + fmt.Println(p.playQueue[0].Metadata().ID) + newNowPlayingIdx := -1 + if p.nowPlayingIdx >= 0 { + nowPlayingID := p.getPlayQueueItemAt(p.nowPlayingIdx).Metadata().ID + for i, tr := range newQueue { + if tr.Metadata().ID == nowPlayingID { + newNowPlayingIdx = i + break + } + } + } + p.shuffle = shuffle + p.setPlayQueue(newQueue) + if p.nowPlayingIdx >= 0 && newNowPlayingIdx == -1 { + return + } + if p.nowPlayingIdx >= 0 { + p.handleNextTrackUpdated() + } + p.nowPlayingIdx = newNowPlayingIdx + + p.invokeNoArgCallbacks(p.onQueueChange) } else { fmt.Println("deshuffling") - p.UpdatePlayQueue(p.playQueue) + fmt.Println(p.playQueue[0].Metadata().ID) + newQueue := deepCopyMediaItemSlice(p.playQueue) + newNowPlayingIdx := -1 + if p.nowPlayingIdx >= 0 { + nowPlayingID := p.getPlayQueueItemAt(p.nowPlayingIdx).Metadata().ID + for i, tr := range newQueue { + if tr.Metadata().ID == nowPlayingID { + newNowPlayingIdx = i + break + } + } + } + p.shuffle = shuffle + p.setPlayQueue(newQueue) + if p.nowPlayingIdx >= 0 && newNowPlayingIdx == -1 { + return + } + if p.nowPlayingIdx >= 0 { + p.handleNextTrackUpdated() + } + p.nowPlayingIdx = newNowPlayingIdx + + p.invokeNoArgCallbacks(p.onQueueChange) } - p.shuffle = shuffle + } func (p *playbackEngine) GetShuffle() bool { From 6a5ec33799d357926e5c62c41e7251ffeafc2369 Mon Sep 17 00:00:00 2001 From: Siiiiinth <60766886+Siiiiinth@users.noreply.github.com> Date: Sun, 25 Jan 2026 18:22:42 +0100 Subject: [PATCH 14/28] use new play queue len func in playbackmanager --- backend/playbackmanager.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index 9bf024a..0e47c37 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -109,7 +109,7 @@ func (p *PlaybackManager) addOnTrackChangeHook() { // enqueue autoplay tracks if enabled and nearing end of queue if p.cfg.Autoplay && !p.pendingAutoplay && totalTime-curTime < 10.0 && - p.NowPlayingIndex() == len(p.engine.playQueue)-1 { + p.NowPlayingIndex() == p.engine.getPlayQueueLength()-1 { p.enqueueAutoplayTracks() } }) @@ -132,7 +132,7 @@ func (p *PlaybackManager) addOnTrackChangeHook() { return } // workaround for https://github.com/dweymouth/supersonic/issues/483 (see above comment) - if p.NowPlayingIndex() != len(p.engine.playQueue) && p.PlaybackStatus().State == player.Playing { + if p.NowPlayingIndex() != p.engine.getPlayQueueLength() && p.PlaybackStatus().State == player.Playing { p.lastPlayTime = 0 go func() { time.Sleep(300 * time.Millisecond) @@ -638,7 +638,7 @@ func (p *PlaybackManager) SetVolume(vol int) { func (p *PlaybackManager) SetAutoplay(autoplay bool) { p.cfg.Autoplay = autoplay - if autoplay && p.NowPlayingIndex() == len(p.engine.playQueue)-1 { + if autoplay && p.NowPlayingIndex() == p.engine.getPlayQueueLength()-1 { p.enqueueAutoplayTracks() } } From f010ef0272bf27745296b37360c00adf4387f28d Mon Sep 17 00:00:00 2001 From: Siiiiinth <60766886+Siiiiinth@users.noreply.github.com> Date: Sun, 25 Jan 2026 18:47:35 +0100 Subject: [PATCH 15/28] checkpoint 3: base shuffle works, but very bad code --- backend/playbackengine.go | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/backend/playbackengine.go b/backend/playbackengine.go index 0601ffa..6f02258 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -255,7 +255,7 @@ func (p *playbackEngine) insertItemsIntoPlayQueueAt(items []mediaprovider.MediaI } func (p *playbackEngine) GetPlayQueueDeepCopy() []mediaprovider.MediaItem { - return deepCopyMediaItemSlice(p.playQueue) // TODO_SHUFFLE: does this also need the shuffled play queue? + return deepCopyMediaItemSlice(p.getPlayQueue()) } // ======================== END PLAY QUEUE FUNCS ============================= @@ -306,20 +306,20 @@ func (p *playbackEngine) GetLoopMode() LoopMode { } func (p *playbackEngine) SetShuffle(shuffle bool) { + //TODO_SHUFFLE: fix very bad code if p.shuffle == shuffle { return } if shuffle { fmt.Println("shuffling") - newQueue := deepCopyMediaItemSlice(p.playQueue) - rand.Shuffle(len(newQueue), func(i, j int) { - newQueue[i], newQueue[j] = newQueue[j], newQueue[i] + shuffledQueue := deepCopyMediaItemSlice(p.playQueue) + rand.Shuffle(len(shuffledQueue), func(i, j int) { + shuffledQueue[i], shuffledQueue[j] = shuffledQueue[j], shuffledQueue[i] }) - fmt.Println(p.playQueue[0].Metadata().ID) newNowPlayingIdx := -1 if p.nowPlayingIdx >= 0 { nowPlayingID := p.getPlayQueueItemAt(p.nowPlayingIdx).Metadata().ID - for i, tr := range newQueue { + for i, tr := range shuffledQueue { if tr.Metadata().ID == nowPlayingID { newNowPlayingIdx = i break @@ -327,7 +327,7 @@ func (p *playbackEngine) SetShuffle(shuffle bool) { } } p.shuffle = shuffle - p.setPlayQueue(newQueue) + p.shuffledPlayQueue = shuffledQueue if p.nowPlayingIdx >= 0 && newNowPlayingIdx == -1 { return } @@ -339,7 +339,6 @@ func (p *playbackEngine) SetShuffle(shuffle bool) { p.invokeNoArgCallbacks(p.onQueueChange) } else { fmt.Println("deshuffling") - fmt.Println(p.playQueue[0].Metadata().ID) newQueue := deepCopyMediaItemSlice(p.playQueue) newNowPlayingIdx := -1 if p.nowPlayingIdx >= 0 { @@ -352,7 +351,7 @@ func (p *playbackEngine) SetShuffle(shuffle bool) { } } p.shuffle = shuffle - p.setPlayQueue(newQueue) + p.playQueue = newQueue if p.nowPlayingIdx >= 0 && newNowPlayingIdx == -1 { return } @@ -363,7 +362,6 @@ func (p *playbackEngine) SetShuffle(shuffle bool) { p.invokeNoArgCallbacks(p.onQueueChange) } - } func (p *playbackEngine) GetShuffle() bool { From 9874ca8c0686e4572311638d284ea296cd9fa56a Mon Sep 17 00:00:00 2001 From: Siiiiinth <60766886+Siiiiinth@users.noreply.github.com> Date: Tue, 27 Jan 2026 14:00:29 +0100 Subject: [PATCH 16/28] todo comments --- backend/playbackengine.go | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/playbackengine.go b/backend/playbackengine.go index 6f02258..0b8ae86 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -610,6 +610,7 @@ func (p *playbackEngine) RemoveTracksFromQueue(idxs []int) { newQueue = append(newQueue, tr) } } + //TODO_SHUFFLE: Removing a track should remove from both shuffled and unshuffled queue p.setPlayQueue(newQueue) p.nowPlayingIdx = newNowPlaying if isPlayingTrackRemoved { From 17e20e1596928b7e6c72b0c43f254ad0e766fc7b Mon Sep 17 00:00:00 2001 From: Siiiiinth <60766886+Siiiiinth@users.noreply.github.com> Date: Sun, 1 Feb 2026 12:01:55 +0100 Subject: [PATCH 17/28] implement remove track for shuffle --- backend/playbackengine.go | 170 +++++++++++++++++++++----------------- 1 file changed, 96 insertions(+), 74 deletions(-) diff --git a/backend/playbackengine.go b/backend/playbackengine.go index 0b8ae86..fd5a979 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -6,6 +6,7 @@ import ( "fmt" "log" "math/rand" + "slices" "strings" "time" @@ -238,11 +239,11 @@ func (p *playbackEngine) clearPlayQueue() { } func (p *playbackEngine) setPlayQueue(items []mediaprovider.MediaItem) { - if p.shuffle { - p.shuffledPlayQueue = items - } else { - p.playQueue = items - } + p.playQueue = items +} + +func (p *playbackEngine) setShuffledPlayQueue(items []mediaprovider.MediaItem) { + p.shuffledPlayQueue = items } func (p *playbackEngine) getPlayQueueItemAt(idx int) mediaprovider.MediaItem { @@ -305,63 +306,45 @@ func (p *playbackEngine) GetLoopMode() LoopMode { return p.loopMode } +// TODO_SHUFFLE: rename? +func (p *playbackEngine) GetNewNowPlayingIdx(items []mediaprovider.MediaItem) int { + newNowPlayingIdx := -1 + if p.nowPlayingIdx >= 0 { + nowPlayingID := p.getPlayQueueItemAt(p.nowPlayingIdx).Metadata().ID + for i, tr := range items { + if tr.Metadata().ID == nowPlayingID { + newNowPlayingIdx = i + break + } + } + } + return newNowPlayingIdx +} + func (p *playbackEngine) SetShuffle(shuffle bool) { - //TODO_SHUFFLE: fix very bad code if p.shuffle == shuffle { return } + + newNowPlayingIdx := -1 if shuffle { - fmt.Println("shuffling") shuffledQueue := deepCopyMediaItemSlice(p.playQueue) rand.Shuffle(len(shuffledQueue), func(i, j int) { shuffledQueue[i], shuffledQueue[j] = shuffledQueue[j], shuffledQueue[i] }) - newNowPlayingIdx := -1 - if p.nowPlayingIdx >= 0 { - nowPlayingID := p.getPlayQueueItemAt(p.nowPlayingIdx).Metadata().ID - for i, tr := range shuffledQueue { - if tr.Metadata().ID == nowPlayingID { - newNowPlayingIdx = i - break - } - } - } - p.shuffle = shuffle + newNowPlayingIdx = p.GetNewNowPlayingIdx(shuffledQueue) p.shuffledPlayQueue = shuffledQueue - if p.nowPlayingIdx >= 0 && newNowPlayingIdx == -1 { - return - } - if p.nowPlayingIdx >= 0 { - p.handleNextTrackUpdated() - } - p.nowPlayingIdx = newNowPlayingIdx - - p.invokeNoArgCallbacks(p.onQueueChange) } else { - fmt.Println("deshuffling") - newQueue := deepCopyMediaItemSlice(p.playQueue) - newNowPlayingIdx := -1 - if p.nowPlayingIdx >= 0 { - nowPlayingID := p.getPlayQueueItemAt(p.nowPlayingIdx).Metadata().ID - for i, tr := range newQueue { - if tr.Metadata().ID == nowPlayingID { - newNowPlayingIdx = i - break - } - } - } - p.shuffle = shuffle - p.playQueue = newQueue - if p.nowPlayingIdx >= 0 && newNowPlayingIdx == -1 { - return - } - if p.nowPlayingIdx >= 0 { - p.handleNextTrackUpdated() - } - p.nowPlayingIdx = newNowPlayingIdx - - p.invokeNoArgCallbacks(p.onQueueChange) + newNowPlayingIdx = p.GetNewNowPlayingIdx(p.playQueue) } + + p.shuffle = shuffle + if p.nowPlayingIdx >= 0 && newNowPlayingIdx == -1 { + return + } + p.handleNextTrackUpdated() + p.nowPlayingIdx = newNowPlayingIdx + p.invokeNoArgCallbacks(p.onQueueChange) } func (p *playbackEngine) GetShuffle() bool { @@ -585,36 +568,29 @@ func (p *playbackEngine) UpdatePlayQueue(items []mediaprovider.MediaItem) error } func (p *playbackEngine) RemoveTracksFromQueue(idxs []int) { - newQueue := make([]mediaprovider.MediaItem, 0, p.getPlayQueueLength()-len(idxs)) - idxSet := sharedutil.ToSet(idxs) isPlayingTrackRemoved := false isNextPlayingTrackremoved := false nowPlaying := p.NowPlayingIndex() newNowPlaying := nowPlaying - for i, tr := range p.getPlayQueue() { - if _, ok := idxSet[i]; ok { - if i < nowPlaying { - // if removing a track earlier than the currently playing one (if any), - // decrement new now playing index by one to account for new position in queue - newNowPlaying-- - } else if i == nowPlaying { - isPlayingTrackRemoved = true - // If we are removing the currently playing track, we need to scrobble it - p.checkScrobble() - p.alreadyScrobbled = true - } else if nowPlaying >= 0 && i == nowPlaying+1 { - isNextPlayingTrackremoved = true - } - } else { - // not removing this track - newQueue = append(newQueue, tr) - } + + if p.shuffle { + ids := p.GetTrackIdsFromIdx(idxs) + newPlayQueue := make([]mediaprovider.MediaItem, 0, p.getPlayQueueLength()-len(idxs)) + p.RemoveTracksFromQueueById(ids, &newPlayQueue) + p.setPlayQueue(newPlayQueue) + + newShuffledQueue := make([]mediaprovider.MediaItem, 0, p.getPlayQueueLength()-len(idxs)) + p.RemoveTracksFromQueueByIdx(idxs, &newShuffledQueue, &newNowPlaying, &isPlayingTrackRemoved, &isNextPlayingTrackremoved) + p.setShuffledPlayQueue(newShuffledQueue) + } else { + newPlayQueue := make([]mediaprovider.MediaItem, 0, p.getPlayQueueLength()-len(idxs)) + p.RemoveTracksFromQueueByIdx(idxs, &newPlayQueue, &newNowPlaying, &isPlayingTrackRemoved, &isNextPlayingTrackremoved) + p.setPlayQueue(newPlayQueue) } - //TODO_SHUFFLE: Removing a track should remove from both shuffled and unshuffled queue - p.setPlayQueue(newQueue) + p.nowPlayingIdx = newNowPlaying if isPlayingTrackRemoved { - if newNowPlaying == len(newQueue) { + if newNowPlaying == p.getPlayQueueLength() { // we had been playing the last track, and removed it p.Stop() } else { @@ -624,7 +600,7 @@ func (p *playbackEngine) RemoveTracksFromQueue(idxs []int) { // setNextTrack and onSongChange callbacks will be handled // when we receive new track event from player } else if isNextPlayingTrackremoved { - if newNowPlaying < len(newQueue)-1 { + if newNowPlaying < p.getPlayQueueLength()-1 { p.handleNextTrackUpdated() } else { // no next track to play @@ -635,6 +611,52 @@ func (p *playbackEngine) RemoveTracksFromQueue(idxs []int) { p.invokeNoArgCallbacks(p.onQueueChange) } +func (p *playbackEngine) RemoveTracksFromQueueByIdx(idxs []int, newQueue *[]mediaprovider.MediaItem, newNowPlaying *int, isPlayingTrackRemoved *bool, isNextPlayingTrackRemoved *bool) { + idxSet := sharedutil.ToSet(idxs) + nowPlaying := p.NowPlayingIndex() + + for i, tr := range p.getPlayQueue() { + if _, ok := idxSet[i]; ok { + if i < nowPlaying { + // if removing a track earlier than the currently playing one (if any), + // decrement new now playing index by one to account for new position in queue + *newNowPlaying-- + } else if i == nowPlaying { + *isPlayingTrackRemoved = true + // If we are removing the currently playing track, we need to scrobble it + p.checkScrobble() + p.alreadyScrobbled = true + } else if nowPlaying >= 0 && i == nowPlaying+1 { + *isNextPlayingTrackRemoved = true + } + } else { + // not removing this track + *newQueue = append(*newQueue, tr) + } + } +} + +func (p *playbackEngine) RemoveTracksFromQueueById(ids []string, newQueue *[]mediaprovider.MediaItem) { + for _, tr := range p.playQueue { //TODO_SHUFFLE: Rework this to use a func for getPlayQueue + 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) + ids = slices.Delete(ids, idx, idx+1) + } else { + // not removing this track + *newQueue = append(*newQueue, tr) + } + } +} + +func (p *playbackEngine) GetTrackIdsFromIdx(idx []int) []string { + ids := make([]string, 0, len(idx)) + for _, v := range idx { + ids = append(ids, p.getPlayQueueItemAt(v).Metadata().ID) + } + return ids +} + func (p *playbackEngine) SetReplayGainOptions(config ReplayGainConfig) { rGainPlayer, ok := p.player.(player.ReplayGainPlayer) if !ok { From c21be4e55f778c546bb7ea0be078e47c6b1e67d7 Mon Sep 17 00:00:00 2001 From: Siiiiinth <60766886+Siiiiinth@users.noreply.github.com> Date: Sun, 1 Feb 2026 12:07:42 +0100 Subject: [PATCH 18/28] fix lint --- backend/playbackengine.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/playbackengine.go b/backend/playbackengine.go index fd5a979..4f2ebb9 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -326,7 +326,7 @@ func (p *playbackEngine) SetShuffle(shuffle bool) { return } - newNowPlayingIdx := -1 + var newNowPlayingIdx int if shuffle { shuffledQueue := deepCopyMediaItemSlice(p.playQueue) rand.Shuffle(len(shuffledQueue), func(i, j int) { From 15caaad5cf474ce2e3a5409a0262764b4a8e67dc Mon Sep 17 00:00:00 2001 From: Siiiiinth <60766886+Siiiiinth@users.noreply.github.com> Date: Sun, 1 Feb 2026 12:34:50 +0100 Subject: [PATCH 19/28] code clean up - remove tracks --- backend/playbackengine.go | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/backend/playbackengine.go b/backend/playbackengine.go index 4f2ebb9..d6e348a 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -332,6 +332,7 @@ func (p *playbackEngine) SetShuffle(shuffle bool) { rand.Shuffle(len(shuffledQueue), func(i, j int) { shuffledQueue[i], shuffledQueue[j] = shuffledQueue[j], shuffledQueue[i] }) + // TODO_SHUFFLE: move now playing track to beginning of queue newNowPlayingIdx = p.GetNewNowPlayingIdx(shuffledQueue) p.shuffledPlayQueue = shuffledQueue } else { @@ -574,9 +575,19 @@ func (p *playbackEngine) RemoveTracksFromQueue(idxs []int) { newNowPlaying := nowPlaying if p.shuffle { + // remove tracks by ID from playQueue ids := p.GetTrackIdsFromIdx(idxs) newPlayQueue := make([]mediaprovider.MediaItem, 0, p.getPlayQueueLength()-len(idxs)) - p.RemoveTracksFromQueueById(ids, &newPlayQueue) + for _, tr := range p.playQueue { //TODO_SHUFFLE: Rework this to use a func for getPlayQueue + 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) + ids = slices.Delete(ids, idx, idx+1) + } else { + // not removing this track + newPlayQueue = append(newPlayQueue, tr) + } + } p.setPlayQueue(newPlayQueue) newShuffledQueue := make([]mediaprovider.MediaItem, 0, p.getPlayQueueLength()-len(idxs)) @@ -636,19 +647,6 @@ func (p *playbackEngine) RemoveTracksFromQueueByIdx(idxs []int, newQueue *[]medi } } -func (p *playbackEngine) RemoveTracksFromQueueById(ids []string, newQueue *[]mediaprovider.MediaItem) { - for _, tr := range p.playQueue { //TODO_SHUFFLE: Rework this to use a func for getPlayQueue - 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) - ids = slices.Delete(ids, idx, idx+1) - } else { - // not removing this track - *newQueue = append(*newQueue, tr) - } - } -} - func (p *playbackEngine) GetTrackIdsFromIdx(idx []int) []string { ids := make([]string, 0, len(idx)) for _, v := range idx { From 1a5fed3adde3db1ea83829b35a15a55237bf9b55 Mon Sep 17 00:00:00 2001 From: Siiiiinth <60766886+Siiiiinth@users.noreply.github.com> Date: Sun, 1 Feb 2026 13:08:27 +0100 Subject: [PATCH 20/28] move current track to front on shuffle --- backend/playbackengine.go | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/backend/playbackengine.go b/backend/playbackengine.go index d6e348a..f71b069 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -229,6 +229,13 @@ func (p *playbackEngine) getPlayQueue() []mediaprovider.MediaItem { return p.playQueue } +func (p *playbackEngine) getActivePlayQueue() []mediaprovider.MediaItem { + if p.shuffle { + return p.shuffledPlayQueue + } + return p.playQueue +} + func (p *playbackEngine) getPlayQueueLength() int { return len(p.getPlayQueue()) } @@ -259,6 +266,10 @@ func (p *playbackEngine) GetPlayQueueDeepCopy() []mediaprovider.MediaItem { return deepCopyMediaItemSlice(p.getPlayQueue()) } +func (p *playbackEngine) GetActivePlayQueueDeepCopy() []mediaprovider.MediaItem { + return deepCopyMediaItemSlice(p.getActivePlayQueue()) +} + // ======================== END PLAY QUEUE FUNCS ============================= func (p *playbackEngine) PlayTrackAt(idx int) error { @@ -332,9 +343,8 @@ func (p *playbackEngine) SetShuffle(shuffle bool) { rand.Shuffle(len(shuffledQueue), func(i, j int) { shuffledQueue[i], shuffledQueue[j] = shuffledQueue[j], shuffledQueue[i] }) - // TODO_SHUFFLE: move now playing track to beginning of queue - newNowPlayingIdx = p.GetNewNowPlayingIdx(shuffledQueue) - p.shuffledPlayQueue = shuffledQueue + p.shuffledPlayQueue = sharedutil.ReorderItems(shuffledQueue, []int{p.GetNewNowPlayingIdx(shuffledQueue)}, 0) + newNowPlayingIdx = 0 } else { newNowPlayingIdx = p.GetNewNowPlayingIdx(p.playQueue) } @@ -554,8 +564,11 @@ func (p *playbackEngine) UpdatePlayQueue(items []mediaprovider.MediaItem) error } } } - - p.setPlayQueue(newQueue) + if p.shuffle { + p.setShuffledPlayQueue(newQueue) + } else { + p.setPlayQueue(newQueue) + } if p.nowPlayingIdx >= 0 && newNowPlayingIdx == -1 { return p.Stop() } From cec32f68e3930295c172524aa385fe8818575fa9 Mon Sep 17 00:00:00 2001 From: Siiiiinth <60766886+Siiiiinth@users.noreply.github.com> Date: Sun, 1 Feb 2026 14:21:57 +0100 Subject: [PATCH 21/28] save shuffle state in config --- backend/config.go | 2 ++ backend/playbackengine.go | 4 ---- backend/playbackmanager.go | 6 ++++-- ui/bottompanel.go | 2 +- ui/widgets/auxcontrols.go | 3 ++- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/backend/config.go b/backend/config.go index 6883f21..d5bd475 100644 --- a/backend/config.go +++ b/backend/config.go @@ -123,6 +123,7 @@ type NowPlayingPageConfig struct { type PlaybackConfig struct { Autoplay bool + Shuffle bool RepeatMode string SkipOneStarWhenShuffling bool SkipKeywordWhenShuffling string @@ -263,6 +264,7 @@ func DefaultConfig(appVersionTag string) *Config { }, Playback: PlaybackConfig{ Autoplay: false, + Shuffle: false, RepeatMode: "None", UseWaveformSeekbar: false, }, diff --git a/backend/playbackengine.go b/backend/playbackengine.go index f71b069..1000cf1 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -358,10 +358,6 @@ func (p *playbackEngine) SetShuffle(shuffle bool) { p.invokeNoArgCallbacks(p.onQueueChange) } -func (p *playbackEngine) GetShuffle() bool { - return p.shuffle -} - func (p *playbackEngine) PlaybackStatus() PlaybackStatus { stat := p.pendingPlayerChangeStatus if !p.pendingPlayerChange { diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index 0e47c37..63bef50 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -373,8 +373,8 @@ func (p *PlaybackManager) LoadAlbum(albumID string, insertQueueMode InsertQueueM return nil } -func (p *PlaybackManager) GetShuffle() bool { - return p.engine.GetShuffle() +func (p *PlaybackManager) IsShuffle() bool { + return p.cfg.Shuffle } // Loads the specified playlist into the play queue. @@ -644,6 +644,8 @@ func (p *PlaybackManager) SetAutoplay(autoplay bool) { } func (p *PlaybackManager) SetShuffle(shuffle bool) { + p.cfg.Shuffle = shuffle + fmt.Println("Setting shuffle in config") p.engine.SetShuffle(shuffle) } diff --git a/ui/bottompanel.go b/ui/bottompanel.go index c4afe87..779424e 100644 --- a/ui/bottompanel.go +++ b/ui/bottompanel.go @@ -102,7 +102,7 @@ func NewBottomPanel(pm *backend.PlaybackManager, im *backend.ImageManager, contr pm.SeekFraction(f) }) - bp.AuxControls = widgets.NewAuxControls(pm.Volume(), pm.GetLoopMode(), pm.IsAutoplay()) + bp.AuxControls = widgets.NewAuxControls(pm.Volume(), pm.GetLoopMode(), pm.IsAutoplay(), pm.IsShuffle()) pm.OnLoopModeChange(func(lm backend.LoopMode) { fyne.Do(func() { bp.AuxControls.SetLoopMode(lm) }) }) diff --git a/ui/widgets/auxcontrols.go b/ui/widgets/auxcontrols.go index b77b140..e63f4c9 100644 --- a/ui/widgets/auxcontrols.go +++ b/ui/widgets/auxcontrols.go @@ -34,7 +34,7 @@ type AuxControls struct { container *fyne.Container } -func NewAuxControls(initialVolume int, initialLoopMode backend.LoopMode, initialAutoplay bool) *AuxControls { +func NewAuxControls(initialVolume int, initialLoopMode backend.LoopMode, initialAutoplay bool, initialShuffle bool) *AuxControls { a := &AuxControls{ VolumeControl: NewVolumeControl(initialVolume), shuffle: NewIconButton(myTheme.ShuffleIcon, nil), @@ -44,6 +44,7 @@ func NewAuxControls(initialVolume int, initialLoopMode backend.LoopMode, initial showQueue: NewIconButton(myTheme.PlayQueueIcon, nil), } + a.shuffle.Highlighted = initialShuffle a.shuffle.IconSize = IconButtonSizeSmaller a.shuffle.SetToolTip(lang.L("Shuffle")) a.shuffle.OnTapped = func() { From 0e0ea5a16551cd0b2c656e9dc4ceda8a0ddde352 Mon Sep 17 00:00:00 2001 From: Siiiiinth <60766886+Siiiiinth@users.noreply.github.com> Date: Sun, 1 Feb 2026 18:46:49 +0100 Subject: [PATCH 22/28] fixed TODOs, first checkpoint saving queue on server --- backend/app.go | 58 +++++++++++++++++++++++++++++--------- backend/playbackengine.go | 39 ++++++++++++------------- backend/playbackmanager.go | 11 ++++++-- backend/savedplayqueue.go | 3 +- sharedutil/sharedutil.go | 8 ++++++ 5 files changed, 81 insertions(+), 38 deletions(-) diff --git a/backend/app.go b/backend/app.go index e731f20..f584f33 100644 --- a/backend/app.go +++ b/backend/app.go @@ -28,11 +28,12 @@ import ( ) const ( - configFile = "config.toml" - portableDir = "supersonic_portable" - savedQueueFile = "saved_queue.json" - themesDir = "themes" - audioCacheSubdir = "audio" + configFile = "config.toml" + portableDir = "supersonic_portable" + savedQueueFile = "saved_queue.json" + savedUnshuffledQueueFile = "saved_unshuffled_queue.json" + themesDir = "themes" + audioCacheSubdir = "audio" ) var ( @@ -604,30 +605,61 @@ func (a *App) SavePlayQueueIfEnabled() { queueServer = qs } } - SavePlayQueue(a.ServerManager.ServerID.String(), a.PlaybackManager, path.Join(a.configDir, savedQueueFile), queueServer) + SavePlayQueue(a.ServerManager.ServerID.String(), a.PlaybackManager.GetActivePlayQueue(), a.PlaybackManager, path.Join(a.configDir, savedQueueFile), queueServer) + if a.Config.Playback.Shuffle { + // if shuffle, also save the unshuffeled queue to enable unshuffling on restarting supersonic + SavePlayQueue(a.ServerManager.ServerID.String(), a.PlaybackManager.GetPlayQueue(), a.PlaybackManager, path.Join(a.configDir, savedUnshuffledQueueFile), queueServer) + } } func (a *App) LoadSavedPlayQueue() error { queueFilePath := path.Join(a.configDir, savedQueueFile) - queue, err := LoadPlayQueue(queueFilePath, a.ServerManager, a.Config.Application.SaveQueueToServer) + playQueue, err := LoadPlayQueue(queueFilePath, a.ServerManager, a.Config.Application.SaveQueueToServer) + var unshuffledPlayQueue *SavedPlayQueue + + if a.Config.Playback.Shuffle { + unshuffledQueueFilePath := path.Join(a.configDir, savedUnshuffledQueueFile) + unshuffledPlayQueue, err = LoadPlayQueue(unshuffledQueueFilePath, a.ServerManager, a.Config.Application.SaveQueueToServer) //unshuffledPlayQueue is only stored locally + + if err != nil { + return err + } + } if err != nil { return err } - if len(queue.Tracks) == 0 { + if len(playQueue.Tracks) == 0 { return nil } - if len(a.PlaybackManager.GetPlayQueue()) > 0 { + if len(a.PlaybackManager.GetActivePlayQueue()) > 0 { // don't restore play queue if the user has already queued new tracks return nil } - a.PlaybackManager.LoadTracks(queue.Tracks, Replace, false) - if queue.TrackIndex >= 0 && queue.TrackIndex < len(queue.Tracks) { + a.PlaybackManager.LoadTracks(playQueue.Tracks, Replace, false) + 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(queue.TrackIndex) + a.PlaybackManager.PlayTrackAt(playQueue.TrackIndex) a.PlaybackManager.Pause() time.Sleep(100 * time.Millisecond) // MPV seek fails if run quickly after - a.PlaybackManager.SeekSeconds(queue.TimePos) + a.PlaybackManager.SeekSeconds(playQueue.TimePos) + } + + if a.Config.Playback.Shuffle { + // check if queue was changed server side + // if playQueue.items same as unshuffledPlayQueue.items + // set shuffle to false, only load normal playqueue + if len(playQueue.Tracks) != len(unshuffledPlayQueue.Tracks) { + // queue was changed server side, no need to compare elements + a.PlaybackManager.SetShuffle(false) + } else { + //TODO_SHUFFLE: implement + + //shuffled := sharedutil.CopyTrackSliceToMediaItemSlice(playQueue.Tracks) + //unshuffled := sharedutil.CopyTrackSliceToMediaItemSlice(unshuffledPlayQueue.Tracks) + + //a.PlaybackManager.engine.setPlayQueue(unshuffled) + } } return nil } diff --git a/backend/playbackengine.go b/backend/playbackengine.go index 1000cf1..72931f4 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -253,6 +253,14 @@ func (p *playbackEngine) setShuffledPlayQueue(items []mediaprovider.MediaItem) { p.shuffledPlayQueue = items } +func (p *playbackEngine) setActivePlayQueue(items []mediaprovider.MediaItem) { + if p.shuffle { + p.setShuffledPlayQueue(items) + } else { + p.setPlayQueue(items) + } +} + func (p *playbackEngine) getPlayQueueItemAt(idx int) mediaprovider.MediaItem { return p.getPlayQueue()[idx] } @@ -266,6 +274,10 @@ func (p *playbackEngine) GetPlayQueueDeepCopy() []mediaprovider.MediaItem { return deepCopyMediaItemSlice(p.getPlayQueue()) } +func (p *playbackEngine) GetShuffledPlayQueueDeepCopy() []mediaprovider.MediaItem { + return deepCopyMediaItemSlice(p.getPlayQueue()) +} + func (p *playbackEngine) GetActivePlayQueueDeepCopy() []mediaprovider.MediaItem { return deepCopyMediaItemSlice(p.getActivePlayQueue()) } @@ -317,8 +329,7 @@ func (p *playbackEngine) GetLoopMode() LoopMode { return p.loopMode } -// TODO_SHUFFLE: rename? -func (p *playbackEngine) GetNewNowPlayingIdx(items []mediaprovider.MediaItem) int { +func (p *playbackEngine) GetNowPlayingIdxFrom(items []mediaprovider.MediaItem) int { newNowPlayingIdx := -1 if p.nowPlayingIdx >= 0 { nowPlayingID := p.getPlayQueueItemAt(p.nowPlayingIdx).Metadata().ID @@ -343,10 +354,10 @@ func (p *playbackEngine) SetShuffle(shuffle bool) { rand.Shuffle(len(shuffledQueue), func(i, j int) { shuffledQueue[i], shuffledQueue[j] = shuffledQueue[j], shuffledQueue[i] }) - p.shuffledPlayQueue = sharedutil.ReorderItems(shuffledQueue, []int{p.GetNewNowPlayingIdx(shuffledQueue)}, 0) + p.shuffledPlayQueue = sharedutil.ReorderItems(shuffledQueue, []int{p.GetNowPlayingIdxFrom(shuffledQueue)}, 0) newNowPlayingIdx = 0 } else { - newNowPlayingIdx = p.GetNewNowPlayingIdx(p.playQueue) + newNowPlayingIdx = p.GetNowPlayingIdxFrom(p.playQueue) } p.shuffle = shuffle @@ -463,7 +474,7 @@ func (p *playbackEngine) LoadItems(items []mediaprovider.MediaItem, insertQueueM // 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 := copyTrackSliceToMediaItemSlice(tracks) + newTracks := sharedutil.CopyTrackSliceToMediaItemSlice(tracks) return p.doLoaditems(newTracks, insertQueueMode, shuffle) } @@ -504,13 +515,7 @@ func (p *playbackEngine) LoadRadioStation(radio *mediaprovider.RadioStation, ins if insertMode == InsertNext { insertIdx = p.nowPlayingIdx + 1 } - new := make([]mediaprovider.MediaItem, p.getPlayQueueLength()+1) - firstHalf := p.playQueue[:insertIdx] - copy(new, firstHalf) - new[len(firstHalf)] = radio - copy(new[len(firstHalf)+1:], p.playQueue[insertIdx:]) //TODO_SHUFFLE: Test shuffle with radio stations - p.setPlayQueue(new) - + p.insertItemsIntoPlayQueueAt([]mediaprovider.MediaItem{radio}, insertIdx) p.invokeNoArgCallbacks(p.onQueueChange) } @@ -587,7 +592,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.playQueue { //TODO_SHUFFLE: Rework this to use a func for getPlayQueue + for _, tr := range p.getPlayQueue() { 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) @@ -954,14 +959,6 @@ func deepCopyMediaItemSlice(tracks []mediaprovider.MediaItem) []mediaprovider.Me return newTracks } -func copyTrackSliceToMediaItemSlice(tracks []*mediaprovider.Track) []mediaprovider.MediaItem { - newTracks := make([]mediaprovider.MediaItem, len(tracks)) - for i, tr := range tracks { - newTracks[i] = tr.Copy() - } - return newTracks -} - func (p *playbackEngine) invokeOnSongChangeCallbacks() { if p.callbacksDisabled { return diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index 63bef50..345eb55 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -390,7 +390,7 @@ func (p *PlaybackManager) LoadPlaylist(playlistID string, insertQueueMode Insert // Load tracks into the play queue. // If replacing the current queue (!appendToQueue), playback will be stopped. func (p *PlaybackManager) LoadTracks(tracks []*mediaprovider.Track, insertQueueMode InsertQueueMode, shuffle bool) { - items := copyTrackSliceToMediaItemSlice(tracks) + items := sharedutil.CopyTrackSliceToMediaItemSlice(tracks) p.cmdQueue.LoadItems(items, insertQueueMode, shuffle) } @@ -570,10 +570,18 @@ func (p *PlaybackManager) fetchAndPlayTracks(fetchFn func() ([]*mediaprovider.Tr } } +func (p *PlaybackManager) GetActivePlayQueue() []mediaprovider.MediaItem { + return p.engine.GetActivePlayQueueDeepCopy() +} + func (p *PlaybackManager) GetPlayQueue() []mediaprovider.MediaItem { return p.engine.GetPlayQueueDeepCopy() } +func (p *PlaybackManager) GetShuffledPlayQueue() []mediaprovider.MediaItem { + return p.engine.GetShuffledPlayQueueDeepCopy() +} + // 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 *PlaybackManager) OnTrackFavoriteStatusChanged(id string, fav bool) { @@ -645,7 +653,6 @@ func (p *PlaybackManager) SetAutoplay(autoplay bool) { func (p *PlaybackManager) SetShuffle(shuffle bool) { p.cfg.Shuffle = shuffle - fmt.Println("Setting shuffle in config") p.engine.SetShuffle(shuffle) } diff --git a/backend/savedplayqueue.go b/backend/savedplayqueue.go index b7ec5ed..6234665 100644 --- a/backend/savedplayqueue.go +++ b/backend/savedplayqueue.go @@ -24,8 +24,7 @@ type serializedSavedPlayQueue struct { // SavePlayQueue saves the current play queue and playback position to a JSON file. // If the provided CanSavePlayQueue server is non-nil, it will also save to the server. -func SavePlayQueue(serverID string, pm *PlaybackManager, filepath string, server mediaprovider.CanSavePlayQueue) error { - queue := pm.GetPlayQueue() +func SavePlayQueue(serverID string, queue []mediaprovider.MediaItem, pm *PlaybackManager, filepath string, server mediaprovider.CanSavePlayQueue) error { stats := pm.PlaybackStatus() trackIdx := pm.NowPlayingIndex() diff --git a/sharedutil/sharedutil.go b/sharedutil/sharedutil.go index e094364..7907d78 100644 --- a/sharedutil/sharedutil.go +++ b/sharedutil/sharedutil.go @@ -186,3 +186,11 @@ func DownloadFileWithContext(ctx context.Context, url string, destPath string) ( return true, nil } + +func CopyTrackSliceToMediaItemSlice(tracks []*mediaprovider.Track) []mediaprovider.MediaItem { + newTracks := make([]mediaprovider.MediaItem, len(tracks)) + for i, tr := range tracks { + newTracks[i] = tr.Copy() + } + return newTracks +} From 9440f65e9378ffb21a8c4ed7a4420f008a3ca424 Mon Sep 17 00:00:00 2001 From: Siiiiinth <60766886+Siiiiinth@users.noreply.github.com> Date: Sun, 1 Feb 2026 18:49:52 +0100 Subject: [PATCH 23/28] lint --- backend/playbackengine.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/backend/playbackengine.go b/backend/playbackengine.go index 72931f4..14f1f65 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -253,14 +253,6 @@ func (p *playbackEngine) setShuffledPlayQueue(items []mediaprovider.MediaItem) { p.shuffledPlayQueue = items } -func (p *playbackEngine) setActivePlayQueue(items []mediaprovider.MediaItem) { - if p.shuffle { - p.setShuffledPlayQueue(items) - } else { - p.setPlayQueue(items) - } -} - func (p *playbackEngine) getPlayQueueItemAt(idx int) mediaprovider.MediaItem { return p.getPlayQueue()[idx] } From acfead5b848889107203d3db13e4c00531b131b7 Mon Sep 17 00:00:00 2001 From: Siiiiinth <60766886+Siiiiinth@users.noreply.github.com> Date: Mon, 9 Feb 2026 08:09:01 +0100 Subject: [PATCH 24/28] wip restore queues on start --- backend/app.go | 45 +++++++++++++++++++++++---------- backend/playbackcommands.go | 6 +++-- backend/playbackengine.go | 43 +++++++++++++++++++++---------- backend/playbackmanager.go | 27 ++++++++++---------- ui/browsing/albumpage.go | 2 +- ui/browsing/favoritespage.go | 2 +- ui/browsing/nowplayingpage.go | 8 +++--- ui/browsing/playlistpage.go | 4 +-- ui/controller/connectactions.go | 14 +++++----- ui/controller/searchdialog.go | 11 ++++---- 10 files changed, 101 insertions(+), 61 deletions(-) diff --git a/backend/app.go b/backend/app.go index f584f33..71856b8 100644 --- a/backend/app.go +++ b/backend/app.go @@ -21,6 +21,7 @@ import ( "github.com/dweymouth/supersonic/backend/player/mpv" "github.com/dweymouth/supersonic/backend/util" "github.com/dweymouth/supersonic/backend/windows" + "github.com/dweymouth/supersonic/sharedutil" "github.com/google/uuid" "github.com/20after4/configdir" @@ -32,6 +33,7 @@ const ( portableDir = "supersonic_portable" savedQueueFile = "saved_queue.json" savedUnshuffledQueueFile = "saved_unshuffled_queue.json" + savedShuffledQueueFile = "saved_shuffled_queue.json" themesDir = "themes" audioCacheSubdir = "audio" ) @@ -607,8 +609,13 @@ func (a *App) SavePlayQueueIfEnabled() { } SavePlayQueue(a.ServerManager.ServerID.String(), a.PlaybackManager.GetActivePlayQueue(), a.PlaybackManager, path.Join(a.configDir, savedQueueFile), queueServer) if a.Config.Playback.Shuffle { - // if shuffle, also save the unshuffeled queue to enable unshuffling on restarting supersonic - SavePlayQueue(a.ServerManager.ServerID.String(), a.PlaybackManager.GetPlayQueue(), a.PlaybackManager, path.Join(a.configDir, savedUnshuffledQueueFile), queueServer) + // if shuffle + // save the unshuffeled queue to enable unshuffling on restarting supersonic + // save the shuffled queue again to enable checking if the playQueue was changed server side on start up + + // both files are just saved locally + SavePlayQueue(a.ServerManager.ServerID.String(), a.PlaybackManager.GetPlayQueue(), a.PlaybackManager, path.Join(a.configDir, savedUnshuffledQueueFile), nil) + SavePlayQueue(a.ServerManager.ServerID.String(), a.PlaybackManager.GetShuffledPlayQueue(), a.PlaybackManager, path.Join(a.configDir, savedShuffledQueueFile), nil) } } @@ -616,10 +623,20 @@ 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 - if a.Config.Playback.Shuffle { + isShuffe := a.Config.Playback.Shuffle + + if isShuffe { unshuffledQueueFilePath := path.Join(a.configDir, savedUnshuffledQueueFile) - unshuffledPlayQueue, err = LoadPlayQueue(unshuffledQueueFilePath, a.ServerManager, a.Config.Application.SaveQueueToServer) //unshuffledPlayQueue is only stored locally + unshuffledPlayQueue, err = LoadPlayQueue(unshuffledQueueFilePath, a.ServerManager, false) + + if err != nil { + return err + } + + shuffledQueueFilePath := path.Join(a.configDir, savedUnshuffledQueueFile) + shuffledPlayQueue, err = LoadPlayQueue(shuffledQueueFilePath, a.ServerManager, false) if err != nil { return err @@ -636,7 +653,7 @@ func (a *App) LoadSavedPlayQueue() error { return nil } - a.PlaybackManager.LoadTracks(playQueue.Tracks, Replace, false) + a.PlaybackManager.LoadTracks(playQueue.Tracks, Replace, Both, false) 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) @@ -645,20 +662,22 @@ func (a *App) LoadSavedPlayQueue() error { a.PlaybackManager.SeekSeconds(playQueue.TimePos) } - if a.Config.Playback.Shuffle { + if isShuffe { // check if queue was changed server side - // if playQueue.items same as unshuffledPlayQueue.items - // set shuffle to false, only load normal playqueue if len(playQueue.Tracks) != len(unshuffledPlayQueue.Tracks) { // queue was changed server side, no need to compare elements a.PlaybackManager.SetShuffle(false) } else { - //TODO_SHUFFLE: implement + serverStatePlayQueue := sharedutil.CopyTrackSliceToMediaItemSlice(playQueue.Tracks) + clientStatePlayQueue := sharedutil.CopyTrackSliceToMediaItemSlice(shuffledPlayQueue.Tracks) - //shuffled := sharedutil.CopyTrackSliceToMediaItemSlice(playQueue.Tracks) - //unshuffled := sharedutil.CopyTrackSliceToMediaItemSlice(unshuffledPlayQueue.Tracks) - - //a.PlaybackManager.engine.setPlayQueue(unshuffled) + if slices.Equal(serverStatePlayQueue, clientStatePlayQueue) { + fmt.Println("Loading unshuffled items into playQueue") + a.PlaybackManager.SetShuffle(true) + a.PlaybackManager.LoadTracks(unshuffledPlayQueue.Tracks, Replace, PlayQueue, false) + } else { + a.PlaybackManager.SetShuffle(false) + } } } return nil diff --git a/backend/playbackcommands.go b/backend/playbackcommands.go index 2632fef..3a79b70 100644 --- a/backend/playbackcommands.go +++ b/backend/playbackcommands.go @@ -35,6 +35,7 @@ type playbackCommand struct { Arg any Arg2 any Arg3 any + Arg4 any OnDone func() } @@ -148,13 +149,14 @@ func (c *playbackCommandQueue) LoadRadioStation(radio *mediaprovider.RadioStatio c.cmdAvailable.Signal() } -func (c *playbackCommandQueue) LoadItems(items []mediaprovider.MediaItem, insertQueueMode InsertQueueMode, shuffle bool) { +func (c *playbackCommandQueue) LoadItems(items []mediaprovider.MediaItem, insertQueueMode InsertQueueMode, queueType QueueType, shuffle bool) { c.mutex.Lock() c.queue = append(c.queue, playbackCommand{ Type: cmdLoadItems, Arg: items, Arg2: insertQueueMode, - Arg3: shuffle, + Arg3: queueType, + Arg4: shuffle, }) c.mutex.Unlock() c.cmdAvailable.Signal() diff --git a/backend/playbackengine.go b/backend/playbackengine.go index 14f1f65..e91410c 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -7,6 +7,7 @@ import ( "log" "math/rand" "slices" + "strconv" "strings" "time" @@ -34,6 +35,14 @@ const ( Append ) +type QueueType int + +const ( + PlayQueue QueueType = iota + ShuffledPlayQueue + Both +) + // The playback loop mode (LoopNone, LoopAll, LoopOne). type LoopMode int @@ -242,7 +251,7 @@ func (p *playbackEngine) getPlayQueueLength() int { func (p *playbackEngine) clearPlayQueue() { p.playQueue = nil - p.SetShuffle(false) + p.shuffledPlayQueue = nil } func (p *playbackEngine) setPlayQueue(items []mediaprovider.MediaItem) { @@ -257,9 +266,17 @@ func (p *playbackEngine) getPlayQueueItemAt(idx int) mediaprovider.MediaItem { return p.getPlayQueue()[idx] } -func (p *playbackEngine) insertItemsIntoPlayQueueAt(items []mediaprovider.MediaItem, idx int) { - p.playQueue = append(p.playQueue[:idx], append(items, p.playQueue[idx:]...)...) - p.shuffledPlayQueue = append(p.shuffledPlayQueue[:idx], append(items, p.shuffledPlayQueue[idx:]...)...) +func (p *playbackEngine) insertItemsIntoPlayQueueAt(items []mediaprovider.MediaItem, idx int, queueType QueueType) { + switch queueType { + case Both: + p.playQueue = append(p.playQueue[:idx], append(items, p.playQueue[idx:]...)...) + p.shuffledPlayQueue = append(p.shuffledPlayQueue[:idx], append(items, p.shuffledPlayQueue[idx:]...)...) + case PlayQueue: + fmt.Println("Inserting into playQueue at: " + strconv.Itoa(idx)) + p.playQueue = append(p.playQueue[:idx], append(items, p.playQueue[idx:]...)...) + case ShuffledPlayQueue: + p.shuffledPlayQueue = append(p.shuffledPlayQueue[:idx], append(items, p.shuffledPlayQueue[idx:]...)...) + } } func (p *playbackEngine) GetPlayQueueDeepCopy() []mediaprovider.MediaItem { @@ -323,7 +340,7 @@ func (p *playbackEngine) GetLoopMode() LoopMode { func (p *playbackEngine) GetNowPlayingIdxFrom(items []mediaprovider.MediaItem) int { newNowPlayingIdx := -1 - if p.nowPlayingIdx >= 0 { + if p.nowPlayingIdx >= 0 && len(items) > p.nowPlayingIdx { nowPlayingID := p.getPlayQueueItemAt(p.nowPlayingIdx).Metadata().ID for i, tr := range items { if tr.Metadata().ID == nowPlayingID { @@ -458,19 +475,19 @@ 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 { +func (p *playbackEngine) LoadItems(items []mediaprovider.MediaItem, insertQueueMode InsertQueueMode, queueType QueueType, shuffle bool) error { newItems := deepCopyMediaItemSlice(items) - return p.doLoaditems(newItems, insertQueueMode, shuffle) + return p.doLoaditems(newItems, insertQueueMode, queueType, 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 { +func (p *playbackEngine) LoadTracks(tracks []*mediaprovider.Track, insertQueueMode InsertQueueMode, queueType QueueType, shuffle bool) error { newTracks := sharedutil.CopyTrackSliceToMediaItemSlice(tracks) - return p.doLoaditems(newTracks, insertQueueMode, shuffle) + return p.doLoaditems(newTracks, insertQueueMode, queueType, shuffle) } -func (p *playbackEngine) doLoaditems(items []mediaprovider.MediaItem, insertQueueMode InsertQueueMode, shuffle bool) error { +func (p *playbackEngine) doLoaditems(items []mediaprovider.MediaItem, insertQueueMode InsertQueueMode, queueType QueueType, shuffle bool) error { if insertQueueMode == Replace { p.player.Stop(false) p.nowPlayingIdx = -1 @@ -480,7 +497,7 @@ func (p *playbackEngine) doLoaditems(items []mediaprovider.MediaItem, insertQueu defer p.handleNextTrackUpdated() } - if shuffle { + if shuffle || p.shuffle { rand.Shuffle(len(items), func(i, j int) { items[i], items[j] = items[j], items[i] }) } @@ -489,7 +506,7 @@ func (p *playbackEngine) doLoaditems(items []mediaprovider.MediaItem, insertQueu insertIdx = p.nowPlayingIdx + 1 } - p.insertItemsIntoPlayQueueAt(items, insertIdx) + p.insertItemsIntoPlayQueueAt(items, insertIdx, queueType) p.invokeNoArgCallbacks(p.onQueueChange) return nil } @@ -507,7 +524,7 @@ func (p *playbackEngine) LoadRadioStation(radio *mediaprovider.RadioStation, ins if insertMode == InsertNext { insertIdx = p.nowPlayingIdx + 1 } - p.insertItemsIntoPlayQueueAt([]mediaprovider.MediaItem{radio}, insertIdx) + p.insertItemsIntoPlayQueueAt([]mediaprovider.MediaItem{radio}, insertIdx, Both) p.invokeNoArgCallbacks(p.onQueueChange) } diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index 345eb55..7acebe1 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -369,7 +369,7 @@ func (p *PlaybackManager) LoadAlbum(albumID string, insertQueueMode InsertQueueM if err != nil { return err } - p.LoadTracks(album.Tracks, insertQueueMode, shuffle) + p.LoadTracks(album.Tracks, insertQueueMode, Both, shuffle) return nil } @@ -383,21 +383,21 @@ func (p *PlaybackManager) LoadPlaylist(playlistID string, insertQueueMode Insert if err != nil { return err } - p.LoadTracks(playlist.Tracks, insertQueueMode, shuffle) + p.LoadTracks(playlist.Tracks, insertQueueMode, Both, shuffle) return nil } // Load tracks into the play queue. // If replacing the current queue (!appendToQueue), playback will be stopped. -func (p *PlaybackManager) LoadTracks(tracks []*mediaprovider.Track, insertQueueMode InsertQueueMode, shuffle bool) { +func (p *PlaybackManager) LoadTracks(tracks []*mediaprovider.Track, insertQueueMode InsertQueueMode, queueType QueueType, shuffle bool) { items := sharedutil.CopyTrackSliceToMediaItemSlice(tracks) - p.cmdQueue.LoadItems(items, insertQueueMode, shuffle) + p.cmdQueue.LoadItems(items, insertQueueMode, queueType, shuffle) } -// Load items into the play queue. +// Load items into the currently active queue. (shuffledPlayQueue/playQueue) // If replacing the current queue (!appendToQueue), playback will be stopped. -func (p *PlaybackManager) LoadItems(items []mediaprovider.MediaItem, insertQueueMode InsertQueueMode, shuffle bool) { - p.cmdQueue.LoadItems(items, insertQueueMode, shuffle) +func (p *PlaybackManager) LoadItems(items []mediaprovider.MediaItem, insertQueueMode InsertQueueMode, queueType QueueType, shuffle bool) { + p.cmdQueue.LoadItems(items, insertQueueMode, queueType, shuffle) } // Replaces the play queue with the given set of tracks. @@ -434,7 +434,7 @@ func (p *PlaybackManager) PlayTrack(trackID string) error { if err != nil { return err } - p.LoadTracks([]*mediaprovider.Track{tr}, Replace, false) + p.LoadTracks([]*mediaprovider.Track{tr}, Replace, Both, false) if p.engine.replayGainCfg.Mode == ReplayGainAuto { p.SetReplayGainMode(player.ReplayGainTrack) } @@ -472,7 +472,7 @@ func (p *PlaybackManager) PlayArtistDiscography(artistID string, shuffleTracks b log.Printf("failed to get artist tracks: %v\n", err) return } - p.LoadTracks(tr, Replace, shuffleTracks) + p.LoadTracks(tr, Replace, Both, shuffleTracks) if p.engine.replayGainCfg.Mode == ReplayGainAuto { if shuffleTracks { p.SetReplayGainMode(player.ReplayGainTrack) @@ -537,7 +537,7 @@ func (p *PlaybackManager) PlayRandomAlbums(genreName string) error { break } if al, err := mp.GetAlbum(al.ID); err == nil { - p.LoadTracks(al.Tracks, insertMode, false) + p.LoadTracks(al.Tracks, insertMode, Both, false) if i == 0 { p.PlayFromBeginning() insertMode = Append @@ -561,7 +561,7 @@ func (p *PlaybackManager) fetchAndPlayTracks(fetchFn func() ([]*mediaprovider.Tr if songs, err := fetchFn(); err != nil { return err } else { - p.LoadTracks(songs, Replace, false) + p.LoadTracks(songs, Replace, Both, false) if p.engine.replayGainCfg.Mode == ReplayGainAuto { p.SetReplayGainMode(player.ReplayGainTrack) } @@ -811,7 +811,7 @@ func (p *PlaybackManager) enqueueAutoplayTracks() { } if len(tracks) > 0 { - p.LoadTracks(tracks, Append, false /*no need to shuffle, already random*/) + p.LoadTracks(tracks, Append, Both, false /*no need to shuffle, already random*/) } }() } @@ -855,7 +855,8 @@ func (p *PlaybackManager) runCmdQueue(ctx context.Context) { err := p.engine.LoadItems( c.Arg.([]mediaprovider.MediaItem), c.Arg2.(InsertQueueMode), - c.Arg3.(bool), + c.Arg3.(QueueType), + c.Arg4.(bool), ) logIfErr("LoadItems", err) case cmdLoadRadioStation: diff --git a/ui/browsing/albumpage.go b/ui/browsing/albumpage.go index 830a8a2..0221df2 100644 --- a/ui/browsing/albumpage.go +++ b/ui/browsing/albumpage.go @@ -260,7 +260,7 @@ func NewAlbumPageHeader(page *AlbumPage) *AlbumPageHeader { go a.page.pm.PlayAlbum(a.page.albumID, 0, false) }) shuffleBtn := widget.NewButtonWithIcon(lang.L("Shuffle"), myTheme.ShuffleIcon, func() { - a.page.pm.LoadTracks(a.page.tracklist.GetTracks(), backend.Replace, true) + a.page.pm.LoadTracks(a.page.tracklist.GetTracks(), backend.Replace, backend.Both, true) a.page.pm.PlayFromBeginning() }) var pop *widget.PopUpMenu diff --git a/ui/browsing/favoritespage.go b/ui/browsing/favoritespage.go index 4ef3000..1708113 100644 --- a/ui/browsing/favoritespage.go +++ b/ui/browsing/favoritespage.go @@ -98,7 +98,7 @@ func (a *FavoritesPage) createHeader(activeBtnIdx int) { widget.NewButtonWithIcon("", myTheme.TracksIcon, a.onShowFavoriteSongs)) a.shuffleBtn = widget.NewButtonWithIcon(lang.L("Shuffle"), myTheme.ShuffleIcon, func() { if tr := a.tracklistOrNil(); tr != nil { - a.pm.LoadTracks(tr.GetTracks(), backend.Replace, true /*shuffle*/) + a.pm.LoadTracks(tr.GetTracks(), backend.Replace, backend.Both, true /*shuffle*/) a.pm.PlayFromBeginning() } }) diff --git a/ui/browsing/nowplayingpage.go b/ui/browsing/nowplayingpage.go index 97716b9..241958c 100644 --- a/ui/browsing/nowplayingpage.go +++ b/ui/browsing/nowplayingpage.go @@ -159,18 +159,18 @@ func NewNowPlayingPage( a.relatedList.OnSetRating = a.queueList.OnSetRating a.relatedList.OnSetFavorite = a.queueList.OnSetFavorite a.relatedList.OnPlayItemAt = func(idx int) { - a.pm.LoadTracks(a.related, backend.Replace, false) + a.pm.LoadTracks(a.related, backend.Replace, backend.Both, false) a.pm.PlayTrackAt(idx) } a.relatedList.OnAddToQueue = func(items []mediaprovider.MediaItem) { - a.pm.LoadItems(items, backend.Append, false) + a.pm.LoadItems(items, backend.Append, backend.Both, false) } a.relatedList.OnPlaySelection = func(items []mediaprovider.MediaItem, shuffle bool) { - a.pm.LoadItems(items, backend.Replace, shuffle) + a.pm.LoadItems(items, backend.Replace, backend.Both, shuffle) a.pm.PlayFromBeginning() } a.relatedList.OnPlaySelectionNext = func(items []mediaprovider.MediaItem) { - a.pm.LoadItems(items, backend.InsertNext, false) + a.pm.LoadItems(items, backend.InsertNext, backend.Both, false) } a.relatedList.OnPlaySongRadio = func(track *mediaprovider.Track) { go func() { diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index 31374e1..e4aff18 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -348,11 +348,11 @@ func NewPlaylistPageHeader(page *PlaylistPage) *PlaylistPageHeader { }) a.editButton.Hidden = true playButton := widget.NewButtonWithIcon(lang.L("Play"), theme.MediaPlayIcon(), func() { - a.page.pm.LoadTracks(a.page.tracks, backend.Replace, false) + a.page.pm.LoadTracks(a.page.tracks, backend.Replace, backend.Both, false) a.page.pm.PlayFromBeginning() }) shuffleBtn := widget.NewButtonWithIcon(lang.L("Shuffle"), myTheme.ShuffleIcon, func() { - a.page.pm.LoadTracks(a.page.tracks, backend.Replace, true) + a.page.pm.LoadTracks(a.page.tracks, backend.Replace, backend.Both, true) a.page.pm.PlayFromBeginning() }) diff --git a/ui/controller/connectactions.go b/ui/controller/connectactions.go index 1f051e5..cc617c4 100644 --- a/ui/controller/connectactions.go +++ b/ui/controller/connectactions.go @@ -25,20 +25,20 @@ func (m *Controller) ConnectTracklistActions(tracklist *widgets.Tracklist) { func (m *Controller) connectTracklistActionsWithReplayGainMode(tracklist *widgets.Tracklist, mode player.ReplayGainMode) { tracklist.OnAddToPlaylist = m.DoAddTracksToPlaylistWorkflow tracklist.OnPlaySelectionNext = func(tracks []*mediaprovider.Track) { - m.App.PlaybackManager.LoadTracks(tracks, backend.InsertNext, false) + m.App.PlaybackManager.LoadTracks(tracks, backend.InsertNext, backend.Both, false) } tracklist.OnAddToQueue = func(tracks []*mediaprovider.Track) { - m.App.PlaybackManager.LoadTracks(tracks, backend.Append, false) + m.App.PlaybackManager.LoadTracks(tracks, backend.Append, backend.Both, false) } tracklist.OnPlayTrackAt = func(idx int) { - m.App.PlaybackManager.LoadTracks(tracklist.GetTracks(), backend.Replace, false) + m.App.PlaybackManager.LoadTracks(tracklist.GetTracks(), backend.Replace, backend.Both, false) if m.App.Config.ReplayGain.Mode == backend.ReplayGainAuto { m.App.PlaybackManager.SetReplayGainMode(mode) } m.App.PlaybackManager.PlayTrackAt(idx) } tracklist.OnPlaySelection = func(tracks []*mediaprovider.Track, shuffle bool) { - m.App.PlaybackManager.LoadTracks(tracks, backend.Replace, shuffle) + m.App.PlaybackManager.LoadTracks(tracks, backend.Replace, backend.Both, shuffle) if m.App.Config.ReplayGain.Mode == backend.ReplayGainAuto { m.App.PlaybackManager.SetReplayGainMode(mode) } @@ -70,7 +70,7 @@ func (m *Controller) connectTracklistActionsWithReplayGainMode(tracklist *widget log.Println("Error getting song radio: ", err) return } - m.App.PlaybackManager.LoadTracks(tracks, backend.Replace, false) + m.App.PlaybackManager.LoadTracks(tracks, backend.Replace, backend.Both, false) if m.App.Config.ReplayGain.Mode == backend.ReplayGainAuto { m.App.PlaybackManager.SetReplayGainMode(mode) } @@ -164,11 +164,11 @@ func (m *Controller) onDownloadAlbum(albumID string) { func (m *Controller) ConnectArtistGridActions(grid *widgets.GridView) { grid.OnShowItemPage = func(id string) { m.NavigateTo(ArtistRoute(id)) } grid.OnPlayNext = func(artistID string) { - go m.App.PlaybackManager.LoadTracks(m.GetArtistTracks(artistID), backend.InsertNext, false) + go m.App.PlaybackManager.LoadTracks(m.GetArtistTracks(artistID), backend.InsertNext, backend.Both, false) } grid.OnPlay = func(artistID string, shuffle bool) { go m.App.PlaybackManager.PlayArtistDiscography(artistID, shuffle) } grid.OnAddToQueue = func(artistID string) { - go m.App.PlaybackManager.LoadTracks(m.GetArtistTracks(artistID), backend.Append, false) + go m.App.PlaybackManager.LoadTracks(m.GetArtistTracks(artistID), backend.Append, backend.Both, false) } grid.OnAddToPlaylist = func(artistID string) { m.DoAddTracksToPlaylistWorkflow( diff --git a/ui/controller/searchdialog.go b/ui/controller/searchdialog.go index b7c44d3..3c4ab65 100644 --- a/ui/controller/searchdialog.go +++ b/ui/controller/searchdialog.go @@ -46,7 +46,7 @@ func (c *Controller) ShowQuickSearch() { case mediaprovider.ContentTypeTrack: c.App.PlaybackManager.LoadTracks( []*mediaprovider.Track{item.(*mediaprovider.Track)}, - backend.Replace, false /*shuffle*/) + backend.Replace, backend.Both, false /*shuffle*/) c.App.PlaybackManager.PlayFromBeginning() case mediaprovider.ContentTypeAlbum: go c.App.PlaybackManager.PlayAlbum(id, 0, shuffle) @@ -69,7 +69,7 @@ func (c *Controller) ShowQuickSearch() { go func() { tracks, err := c.GetSongRadioTracks(track) if err != nil { - c.App.PlaybackManager.LoadTracks(tracks, backend.Replace, false) + c.App.PlaybackManager.LoadTracks(tracks, backend.Replace, backend.Both, false) c.App.PlaybackManager.PlayFromBeginning() } }() @@ -99,6 +99,7 @@ func (c *Controller) ShowQuickSearch() { func (c *Controller) handleSearchDialogOnAddToQueue(t mediaprovider.ContentType, id string, item any, next bool) { insertMode := backend.Append + queueType := backend.Both if next { insertMode = backend.InsertNext } @@ -106,13 +107,13 @@ func (c *Controller) handleSearchDialogOnAddToQueue(t mediaprovider.ContentType, case mediaprovider.ContentTypeTrack: c.App.PlaybackManager.LoadTracks( []*mediaprovider.Track{item.(*mediaprovider.Track)}, - insertMode, false /*shuffle*/) + insertMode, queueType, false /*shuffle*/) case mediaprovider.ContentTypeAlbum: go c.App.PlaybackManager.LoadAlbum(id, insertMode, false) case mediaprovider.ContentTypeArtist: go func() { tracks := c.GetArtistTracks(id) - c.App.PlaybackManager.LoadTracks(tracks, insertMode, false) + c.App.PlaybackManager.LoadTracks(tracks, insertMode, queueType, false) }() case mediaprovider.ContentTypePlaylist: go c.App.PlaybackManager.LoadPlaylist(id, insertMode, false) @@ -120,7 +121,7 @@ func (c *Controller) handleSearchDialogOnAddToQueue(t mediaprovider.ContentType, go func() { tr, err := c.App.ServerManager.Server.GetRandomTracks(id /*genre name*/, c.App.Config.Application.EnqueueBatchSize) if err != nil { - c.App.PlaybackManager.LoadTracks(tr, insertMode, false /*shuffle*/) + c.App.PlaybackManager.LoadTracks(tr, insertMode, queueType, false /*shuffle*/) } }() case mediaprovider.ContentTypeRadioStation: From c09ec3673a75ce4c80a5930fa0020839fb525e55 Mon Sep 17 00:00:00 2001 From: Siiiiinth <60766886+Siiiiinth@users.noreply.github.com> Date: Mon, 9 Feb 2026 13:43:36 +0100 Subject: [PATCH 25/28] fixed various bugs with inserting and adding tracks --- backend/app.go | 31 ++++++++++++------------ backend/playbackcommands.go | 6 ++--- backend/playbackengine.go | 43 ++++++++++++++++++++++----------- backend/playbackmanager.go | 25 +++++++++---------- ui/browsing/albumpage.go | 2 +- ui/browsing/favoritespage.go | 2 +- ui/browsing/nowplayingpage.go | 8 +++--- ui/browsing/playlistpage.go | 4 +-- ui/controller/connectactions.go | 14 +++++------ ui/controller/searchdialog.go | 11 ++++----- 10 files changed, 78 insertions(+), 68 deletions(-) diff --git a/backend/app.go b/backend/app.go index 71856b8..a7936b3 100644 --- a/backend/app.go +++ b/backend/app.go @@ -46,19 +46,19 @@ var ( ) type App struct { - Config *Config - ServerManager *ServerManager - LyricsManager *LyricsManager - ImageManager *ImageManager - AudioCache *AudioCache - AutoEQManager *AutoEQManager - EQPresetManager *EQPresetManager - PlaybackManager *PlaybackManager - LocalPlayer *mpv.Player - UpdateChecker UpdateChecker - MPRISHandler *MPRISHandler - WinSMTC *windows.SMTC - ipcServer ipc.IPCServer + Config *Config + ServerManager *ServerManager + LyricsManager *LyricsManager + ImageManager *ImageManager + AudioCache *AudioCache + AutoEQManager *AutoEQManager + EQPresetManager *EQPresetManager + PlaybackManager *PlaybackManager + LocalPlayer *mpv.Player + UpdateChecker UpdateChecker + MPRISHandler *MPRISHandler + WinSMTC *windows.SMTC + ipcServer ipc.IPCServer // UI callbacks to be set in main OnReactivate func() @@ -653,7 +653,7 @@ func (a *App) LoadSavedPlayQueue() error { return nil } - a.PlaybackManager.LoadTracks(playQueue.Tracks, Replace, Both, false) + a.PlaybackManager.LoadTracks(playQueue.Tracks, Replace, false) 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) @@ -673,8 +673,7 @@ func (a *App) LoadSavedPlayQueue() error { if slices.Equal(serverStatePlayQueue, clientStatePlayQueue) { fmt.Println("Loading unshuffled items into playQueue") - a.PlaybackManager.SetShuffle(true) - a.PlaybackManager.LoadTracks(unshuffledPlayQueue.Tracks, Replace, PlayQueue, false) + a.PlaybackManager.LoadTracks(unshuffledPlayQueue.Tracks, Replace, false) } else { a.PlaybackManager.SetShuffle(false) } diff --git a/backend/playbackcommands.go b/backend/playbackcommands.go index 3a79b70..2632fef 100644 --- a/backend/playbackcommands.go +++ b/backend/playbackcommands.go @@ -35,7 +35,6 @@ type playbackCommand struct { Arg any Arg2 any Arg3 any - Arg4 any OnDone func() } @@ -149,14 +148,13 @@ func (c *playbackCommandQueue) LoadRadioStation(radio *mediaprovider.RadioStatio c.cmdAvailable.Signal() } -func (c *playbackCommandQueue) LoadItems(items []mediaprovider.MediaItem, insertQueueMode InsertQueueMode, queueType QueueType, shuffle bool) { +func (c *playbackCommandQueue) LoadItems(items []mediaprovider.MediaItem, insertQueueMode InsertQueueMode, shuffle bool) { c.mutex.Lock() c.queue = append(c.queue, playbackCommand{ Type: cmdLoadItems, Arg: items, Arg2: insertQueueMode, - Arg3: queueType, - Arg4: shuffle, + Arg3: shuffle, }) c.mutex.Unlock() c.cmdAvailable.Signal() diff --git a/backend/playbackengine.go b/backend/playbackengine.go index e91410c..e38ef20 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -7,7 +7,6 @@ import ( "log" "math/rand" "slices" - "strconv" "strings" "time" @@ -272,7 +271,6 @@ func (p *playbackEngine) insertItemsIntoPlayQueueAt(items []mediaprovider.MediaI p.playQueue = append(p.playQueue[:idx], append(items, p.playQueue[idx:]...)...) p.shuffledPlayQueue = append(p.shuffledPlayQueue[:idx], append(items, p.shuffledPlayQueue[idx:]...)...) case PlayQueue: - fmt.Println("Inserting into playQueue at: " + strconv.Itoa(idx)) p.playQueue = append(p.playQueue[:idx], append(items, p.playQueue[idx:]...)...) case ShuffledPlayQueue: p.shuffledPlayQueue = append(p.shuffledPlayQueue[:idx], append(items, p.shuffledPlayQueue[idx:]...)...) @@ -475,19 +473,21 @@ 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, queueType QueueType, shuffle bool) error { +func (p *playbackEngine) LoadItems(items []mediaprovider.MediaItem, insertQueueMode InsertQueueMode, shuffle bool) error { newItems := deepCopyMediaItemSlice(items) - return p.doLoaditems(newItems, insertQueueMode, queueType, shuffle) + 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, queueType QueueType, shuffle bool) error { +func (p *playbackEngine) LoadTracks(tracks []*mediaprovider.Track, insertQueueMode InsertQueueMode, shuffle bool) error { newTracks := sharedutil.CopyTrackSliceToMediaItemSlice(tracks) - return p.doLoaditems(newTracks, insertQueueMode, queueType, shuffle) + return p.doLoaditems(newTracks, insertQueueMode, shuffle) } -func (p *playbackEngine) doLoaditems(items []mediaprovider.MediaItem, insertQueueMode InsertQueueMode, queueType QueueType, shuffle bool) error { +func (p *playbackEngine) doLoaditems(items []mediaprovider.MediaItem, insertQueueMode InsertQueueMode, shuffle bool) error { + queueType := PlayQueue + if insertQueueMode == Replace { p.player.Stop(false) p.nowPlayingIdx = -1 @@ -497,16 +497,31 @@ func (p *playbackEngine) doLoaditems(items []mediaprovider.MediaItem, insertQueu defer p.handleNextTrackUpdated() } - if shuffle || p.shuffle { - rand.Shuffle(len(items), func(i, j int) { items[i], items[j] = items[j], items[i] }) - } - insertIdx := p.getPlayQueueLength() - if insertQueueMode == InsertNext { - insertIdx = p.nowPlayingIdx + 1 + + if insertQueueMode == Replace { + if shuffle || p.shuffle { + shuffledItems := deepCopyMediaItemSlice(items) + rand.Shuffle(len(shuffledItems), func(i, j int) { shuffledItems[i], shuffledItems[j] = shuffledItems[j], shuffledItems[i] }) + + p.insertItemsIntoPlayQueueAt(items, insertIdx, PlayQueue) + p.insertItemsIntoPlayQueueAt(shuffledItems, insertIdx, ShuffledPlayQueue) + } else { + p.insertItemsIntoPlayQueueAt(items, insertIdx, queueType) + } + } else { + if shuffle { + rand.Shuffle(len(items), func(i, j int) { items[i], items[j] = items[j], items[i] }) + } + if insertQueueMode == InsertNext { + insertIdx = p.nowPlayingIdx + 1 + } + if p.shuffle { + queueType = Both + } + p.insertItemsIntoPlayQueueAt(items, insertIdx, queueType) } - p.insertItemsIntoPlayQueueAt(items, insertIdx, queueType) p.invokeNoArgCallbacks(p.onQueueChange) return nil } diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index 7acebe1..d594cf5 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -369,7 +369,7 @@ func (p *PlaybackManager) LoadAlbum(albumID string, insertQueueMode InsertQueueM if err != nil { return err } - p.LoadTracks(album.Tracks, insertQueueMode, Both, shuffle) + p.LoadTracks(album.Tracks, insertQueueMode, shuffle) return nil } @@ -383,21 +383,21 @@ func (p *PlaybackManager) LoadPlaylist(playlistID string, insertQueueMode Insert if err != nil { return err } - p.LoadTracks(playlist.Tracks, insertQueueMode, Both, shuffle) + p.LoadTracks(playlist.Tracks, insertQueueMode, shuffle) return nil } // Load tracks into the play queue. // If replacing the current queue (!appendToQueue), playback will be stopped. -func (p *PlaybackManager) LoadTracks(tracks []*mediaprovider.Track, insertQueueMode InsertQueueMode, queueType QueueType, shuffle bool) { +func (p *PlaybackManager) LoadTracks(tracks []*mediaprovider.Track, insertQueueMode InsertQueueMode, shuffle bool) { items := sharedutil.CopyTrackSliceToMediaItemSlice(tracks) - p.cmdQueue.LoadItems(items, insertQueueMode, queueType, shuffle) + 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) LoadItems(items []mediaprovider.MediaItem, insertQueueMode InsertQueueMode, queueType QueueType, shuffle bool) { - p.cmdQueue.LoadItems(items, insertQueueMode, queueType, shuffle) +func (p *PlaybackManager) LoadItems(items []mediaprovider.MediaItem, insertQueueMode InsertQueueMode, shuffle bool) { + p.cmdQueue.LoadItems(items, insertQueueMode, shuffle) } // Replaces the play queue with the given set of tracks. @@ -434,7 +434,7 @@ func (p *PlaybackManager) PlayTrack(trackID string) error { if err != nil { return err } - p.LoadTracks([]*mediaprovider.Track{tr}, Replace, Both, false) + p.LoadTracks([]*mediaprovider.Track{tr}, Replace, false) if p.engine.replayGainCfg.Mode == ReplayGainAuto { p.SetReplayGainMode(player.ReplayGainTrack) } @@ -472,7 +472,7 @@ func (p *PlaybackManager) PlayArtistDiscography(artistID string, shuffleTracks b log.Printf("failed to get artist tracks: %v\n", err) return } - p.LoadTracks(tr, Replace, Both, shuffleTracks) + p.LoadTracks(tr, Replace, shuffleTracks) if p.engine.replayGainCfg.Mode == ReplayGainAuto { if shuffleTracks { p.SetReplayGainMode(player.ReplayGainTrack) @@ -537,7 +537,7 @@ func (p *PlaybackManager) PlayRandomAlbums(genreName string) error { break } if al, err := mp.GetAlbum(al.ID); err == nil { - p.LoadTracks(al.Tracks, insertMode, Both, false) + p.LoadTracks(al.Tracks, insertMode, false) if i == 0 { p.PlayFromBeginning() insertMode = Append @@ -561,7 +561,7 @@ func (p *PlaybackManager) fetchAndPlayTracks(fetchFn func() ([]*mediaprovider.Tr if songs, err := fetchFn(); err != nil { return err } else { - p.LoadTracks(songs, Replace, Both, false) + p.LoadTracks(songs, Replace, false) if p.engine.replayGainCfg.Mode == ReplayGainAuto { p.SetReplayGainMode(player.ReplayGainTrack) } @@ -811,7 +811,7 @@ func (p *PlaybackManager) enqueueAutoplayTracks() { } if len(tracks) > 0 { - p.LoadTracks(tracks, Append, Both, false /*no need to shuffle, already random*/) + p.LoadTracks(tracks, Append, false /*no need to shuffle, already random*/) } }() } @@ -855,8 +855,7 @@ func (p *PlaybackManager) runCmdQueue(ctx context.Context) { err := p.engine.LoadItems( c.Arg.([]mediaprovider.MediaItem), c.Arg2.(InsertQueueMode), - c.Arg3.(QueueType), - c.Arg4.(bool), + c.Arg3.(bool), ) logIfErr("LoadItems", err) case cmdLoadRadioStation: diff --git a/ui/browsing/albumpage.go b/ui/browsing/albumpage.go index 0221df2..830a8a2 100644 --- a/ui/browsing/albumpage.go +++ b/ui/browsing/albumpage.go @@ -260,7 +260,7 @@ func NewAlbumPageHeader(page *AlbumPage) *AlbumPageHeader { go a.page.pm.PlayAlbum(a.page.albumID, 0, false) }) shuffleBtn := widget.NewButtonWithIcon(lang.L("Shuffle"), myTheme.ShuffleIcon, func() { - a.page.pm.LoadTracks(a.page.tracklist.GetTracks(), backend.Replace, backend.Both, true) + a.page.pm.LoadTracks(a.page.tracklist.GetTracks(), backend.Replace, true) a.page.pm.PlayFromBeginning() }) var pop *widget.PopUpMenu diff --git a/ui/browsing/favoritespage.go b/ui/browsing/favoritespage.go index 1708113..4ef3000 100644 --- a/ui/browsing/favoritespage.go +++ b/ui/browsing/favoritespage.go @@ -98,7 +98,7 @@ func (a *FavoritesPage) createHeader(activeBtnIdx int) { widget.NewButtonWithIcon("", myTheme.TracksIcon, a.onShowFavoriteSongs)) a.shuffleBtn = widget.NewButtonWithIcon(lang.L("Shuffle"), myTheme.ShuffleIcon, func() { if tr := a.tracklistOrNil(); tr != nil { - a.pm.LoadTracks(tr.GetTracks(), backend.Replace, backend.Both, true /*shuffle*/) + a.pm.LoadTracks(tr.GetTracks(), backend.Replace, true /*shuffle*/) a.pm.PlayFromBeginning() } }) diff --git a/ui/browsing/nowplayingpage.go b/ui/browsing/nowplayingpage.go index 241958c..97716b9 100644 --- a/ui/browsing/nowplayingpage.go +++ b/ui/browsing/nowplayingpage.go @@ -159,18 +159,18 @@ func NewNowPlayingPage( a.relatedList.OnSetRating = a.queueList.OnSetRating a.relatedList.OnSetFavorite = a.queueList.OnSetFavorite a.relatedList.OnPlayItemAt = func(idx int) { - a.pm.LoadTracks(a.related, backend.Replace, backend.Both, false) + a.pm.LoadTracks(a.related, backend.Replace, false) a.pm.PlayTrackAt(idx) } a.relatedList.OnAddToQueue = func(items []mediaprovider.MediaItem) { - a.pm.LoadItems(items, backend.Append, backend.Both, false) + a.pm.LoadItems(items, backend.Append, false) } a.relatedList.OnPlaySelection = func(items []mediaprovider.MediaItem, shuffle bool) { - a.pm.LoadItems(items, backend.Replace, backend.Both, shuffle) + a.pm.LoadItems(items, backend.Replace, shuffle) a.pm.PlayFromBeginning() } a.relatedList.OnPlaySelectionNext = func(items []mediaprovider.MediaItem) { - a.pm.LoadItems(items, backend.InsertNext, backend.Both, false) + a.pm.LoadItems(items, backend.InsertNext, false) } a.relatedList.OnPlaySongRadio = func(track *mediaprovider.Track) { go func() { diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index e4aff18..31374e1 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -348,11 +348,11 @@ func NewPlaylistPageHeader(page *PlaylistPage) *PlaylistPageHeader { }) a.editButton.Hidden = true playButton := widget.NewButtonWithIcon(lang.L("Play"), theme.MediaPlayIcon(), func() { - a.page.pm.LoadTracks(a.page.tracks, backend.Replace, backend.Both, false) + a.page.pm.LoadTracks(a.page.tracks, backend.Replace, false) a.page.pm.PlayFromBeginning() }) shuffleBtn := widget.NewButtonWithIcon(lang.L("Shuffle"), myTheme.ShuffleIcon, func() { - a.page.pm.LoadTracks(a.page.tracks, backend.Replace, backend.Both, true) + a.page.pm.LoadTracks(a.page.tracks, backend.Replace, true) a.page.pm.PlayFromBeginning() }) diff --git a/ui/controller/connectactions.go b/ui/controller/connectactions.go index cc617c4..1f051e5 100644 --- a/ui/controller/connectactions.go +++ b/ui/controller/connectactions.go @@ -25,20 +25,20 @@ func (m *Controller) ConnectTracklistActions(tracklist *widgets.Tracklist) { func (m *Controller) connectTracklistActionsWithReplayGainMode(tracklist *widgets.Tracklist, mode player.ReplayGainMode) { tracklist.OnAddToPlaylist = m.DoAddTracksToPlaylistWorkflow tracklist.OnPlaySelectionNext = func(tracks []*mediaprovider.Track) { - m.App.PlaybackManager.LoadTracks(tracks, backend.InsertNext, backend.Both, false) + m.App.PlaybackManager.LoadTracks(tracks, backend.InsertNext, false) } tracklist.OnAddToQueue = func(tracks []*mediaprovider.Track) { - m.App.PlaybackManager.LoadTracks(tracks, backend.Append, backend.Both, false) + m.App.PlaybackManager.LoadTracks(tracks, backend.Append, false) } tracklist.OnPlayTrackAt = func(idx int) { - m.App.PlaybackManager.LoadTracks(tracklist.GetTracks(), backend.Replace, backend.Both, false) + m.App.PlaybackManager.LoadTracks(tracklist.GetTracks(), backend.Replace, false) if m.App.Config.ReplayGain.Mode == backend.ReplayGainAuto { m.App.PlaybackManager.SetReplayGainMode(mode) } m.App.PlaybackManager.PlayTrackAt(idx) } tracklist.OnPlaySelection = func(tracks []*mediaprovider.Track, shuffle bool) { - m.App.PlaybackManager.LoadTracks(tracks, backend.Replace, backend.Both, shuffle) + m.App.PlaybackManager.LoadTracks(tracks, backend.Replace, shuffle) if m.App.Config.ReplayGain.Mode == backend.ReplayGainAuto { m.App.PlaybackManager.SetReplayGainMode(mode) } @@ -70,7 +70,7 @@ func (m *Controller) connectTracklistActionsWithReplayGainMode(tracklist *widget log.Println("Error getting song radio: ", err) return } - m.App.PlaybackManager.LoadTracks(tracks, backend.Replace, backend.Both, false) + m.App.PlaybackManager.LoadTracks(tracks, backend.Replace, false) if m.App.Config.ReplayGain.Mode == backend.ReplayGainAuto { m.App.PlaybackManager.SetReplayGainMode(mode) } @@ -164,11 +164,11 @@ func (m *Controller) onDownloadAlbum(albumID string) { func (m *Controller) ConnectArtistGridActions(grid *widgets.GridView) { grid.OnShowItemPage = func(id string) { m.NavigateTo(ArtistRoute(id)) } grid.OnPlayNext = func(artistID string) { - go m.App.PlaybackManager.LoadTracks(m.GetArtistTracks(artistID), backend.InsertNext, backend.Both, false) + go m.App.PlaybackManager.LoadTracks(m.GetArtistTracks(artistID), backend.InsertNext, false) } grid.OnPlay = func(artistID string, shuffle bool) { go m.App.PlaybackManager.PlayArtistDiscography(artistID, shuffle) } grid.OnAddToQueue = func(artistID string) { - go m.App.PlaybackManager.LoadTracks(m.GetArtistTracks(artistID), backend.Append, backend.Both, false) + go m.App.PlaybackManager.LoadTracks(m.GetArtistTracks(artistID), backend.Append, false) } grid.OnAddToPlaylist = func(artistID string) { m.DoAddTracksToPlaylistWorkflow( diff --git a/ui/controller/searchdialog.go b/ui/controller/searchdialog.go index 3c4ab65..b7c44d3 100644 --- a/ui/controller/searchdialog.go +++ b/ui/controller/searchdialog.go @@ -46,7 +46,7 @@ func (c *Controller) ShowQuickSearch() { case mediaprovider.ContentTypeTrack: c.App.PlaybackManager.LoadTracks( []*mediaprovider.Track{item.(*mediaprovider.Track)}, - backend.Replace, backend.Both, false /*shuffle*/) + backend.Replace, false /*shuffle*/) c.App.PlaybackManager.PlayFromBeginning() case mediaprovider.ContentTypeAlbum: go c.App.PlaybackManager.PlayAlbum(id, 0, shuffle) @@ -69,7 +69,7 @@ func (c *Controller) ShowQuickSearch() { go func() { tracks, err := c.GetSongRadioTracks(track) if err != nil { - c.App.PlaybackManager.LoadTracks(tracks, backend.Replace, backend.Both, false) + c.App.PlaybackManager.LoadTracks(tracks, backend.Replace, false) c.App.PlaybackManager.PlayFromBeginning() } }() @@ -99,7 +99,6 @@ func (c *Controller) ShowQuickSearch() { func (c *Controller) handleSearchDialogOnAddToQueue(t mediaprovider.ContentType, id string, item any, next bool) { insertMode := backend.Append - queueType := backend.Both if next { insertMode = backend.InsertNext } @@ -107,13 +106,13 @@ func (c *Controller) handleSearchDialogOnAddToQueue(t mediaprovider.ContentType, case mediaprovider.ContentTypeTrack: c.App.PlaybackManager.LoadTracks( []*mediaprovider.Track{item.(*mediaprovider.Track)}, - insertMode, queueType, false /*shuffle*/) + insertMode, false /*shuffle*/) case mediaprovider.ContentTypeAlbum: go c.App.PlaybackManager.LoadAlbum(id, insertMode, false) case mediaprovider.ContentTypeArtist: go func() { tracks := c.GetArtistTracks(id) - c.App.PlaybackManager.LoadTracks(tracks, insertMode, queueType, false) + c.App.PlaybackManager.LoadTracks(tracks, insertMode, false) }() case mediaprovider.ContentTypePlaylist: go c.App.PlaybackManager.LoadPlaylist(id, insertMode, false) @@ -121,7 +120,7 @@ func (c *Controller) handleSearchDialogOnAddToQueue(t mediaprovider.ContentType, go func() { tr, err := c.App.ServerManager.Server.GetRandomTracks(id /*genre name*/, c.App.Config.Application.EnqueueBatchSize) if err != nil { - c.App.PlaybackManager.LoadTracks(tr, insertMode, queueType, false /*shuffle*/) + c.App.PlaybackManager.LoadTracks(tr, insertMode, false /*shuffle*/) } }() case mediaprovider.ContentTypeRadioStation: From d59d7ac529fa92f930b0ed0bba0a48584558f93f Mon Sep 17 00:00:00 2001 From: Siiiiinth <60766886+Siiiiinth@users.noreply.github.com> Date: Mon, 9 Feb 2026 18:48:50 +0100 Subject: [PATCH 26/28] load full queue state on start --- backend/app.go | 41 ++++++++++++------------ backend/playbackcommands.go | 14 +++++++- backend/playbackengine.go | 60 +++++++++++++++++++++++------------ backend/playbackmanager.go | 19 ++++++++++- ui/bottompanel.go | 3 ++ ui/browsing/nowplayingpage.go | 2 +- ui/controller/controller.go | 4 +-- ui/mainwindow.go | 2 +- 8 files changed, 99 insertions(+), 46 deletions(-) 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) From ad18e304d536ccf83b661b87fe074797b7d4a95e Mon Sep 17 00:00:00 2001 From: Siiiiinth <60766886+Siiiiinth@users.noreply.github.com> Date: Mon, 9 Feb 2026 19:12:19 +0100 Subject: [PATCH 27/28] fixed bug in SetShuffle --- backend/playbackengine.go | 48 +++++++++++++++++++++++++-------------- sharedutil/sharedutil.go | 3 +++ 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/backend/playbackengine.go b/backend/playbackengine.go index b04fe39..0664c65 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -342,40 +342,54 @@ func (p *playbackEngine) GetLoopMode() LoopMode { return p.loopMode } -func (p *playbackEngine) GetNowPlayingIdxFrom(items []mediaprovider.MediaItem) int { - newNowPlayingIdx := -1 - if p.nowPlayingIdx >= 0 && len(items) > p.nowPlayingIdx { - nowPlayingID := p.getPlayQueueItemAt(p.nowPlayingIdx).Metadata().ID - for i, tr := range items { - if tr.Metadata().ID == nowPlayingID { - newNowPlayingIdx = i - break - } +func (p *playbackEngine) GetTrackIdxByIdFrom(items []mediaprovider.MediaItem, id string) int { + foundIdx := -1 + for i, tr := range items { + if tr.Metadata().ID == id { + foundIdx = i + break } } - return newNowPlayingIdx + return foundIdx } func (p *playbackEngine) SetShuffle(shuffle bool) { - for _, cb := range p.onShuffleChange { - cb(shuffle) - } if p.shuffle == shuffle { return } + + for _, cb := range p.onShuffleChange { + cb(shuffle) + } + p.shuffle = shuffle + + // guard against changing shuffle with an empty queue + if p.getPlayQueue() == nil || len(p.getPlayQueue()) == 0 { + return + } + 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.setShuffledPlayQueue(sharedutil.ReorderItems(shuffledQueue, []int{p.GetNowPlayingIdxFrom(shuffledQueue)}, 0)) + if p.nowPlayingIdx >= 0 && len(p.getPlayQueue()) > p.nowPlayingIdx { + nowPlayingID := p.getPlayQueue()[p.nowPlayingIdx].Metadata().ID + p.setShuffledPlayQueue(sharedutil.ReorderItems(shuffledQueue, []int{p.GetTrackIdxByIdFrom(shuffledQueue, nowPlayingID)}, 0)) + } else { + return + } } else { - newNowPlayingIdx = p.GetNowPlayingIdxFrom(p.playQueue) - } + if p.nowPlayingIdx >= 0 && len(p.getShuffledPlayQueue()) > p.nowPlayingIdx { + nowPlayingID := p.getShuffledPlayQueue()[p.nowPlayingIdx].Metadata().ID + newNowPlayingIdx = p.GetTrackIdxByIdFrom(p.playQueue, nowPlayingID) + } else { + return + } - p.shuffle = shuffle + } if p.nowPlayingIdx >= 0 && newNowPlayingIdx == -1 { return diff --git a/sharedutil/sharedutil.go b/sharedutil/sharedutil.go index 7907d78..0e4051e 100644 --- a/sharedutil/sharedutil.go +++ b/sharedutil/sharedutil.go @@ -112,6 +112,9 @@ func TracksToIDs(tracks []*mediaprovider.Track) []string { // Reorder items and return a new track slice. // idxToMove must contain only valid indexes into tracks, and no repeats func ReorderItems[T any](items []T, idxToMove []int, insertIdx int) []T { + if len(items) < 2 { + return items + } idxToMoveSet := ToSet(idxToMove) newItems := make([]T, 0, len(items)) From 16adc4b4fdfc03fc3a3a253709dbeafc4d232570 Mon Sep 17 00:00:00 2001 From: Siiiiinth <60766886+Siiiiinth@users.noreply.github.com> Date: Sun, 15 Feb 2026 10:16:04 +0100 Subject: [PATCH 28/28] fixed track removal --- backend/playbackengine.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/playbackengine.go b/backend/playbackengine.go index 0664c65..bdcab00 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -650,7 +650,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.getActivePlayQueue() { + for _, tr := range p.getPlayQueue() { 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)