load full queue state on start

This commit is contained in:
Siiiiinth
2026-02-15 09:29:48 -08:00
committed by Drew Weymouth
parent c09ec3673a
commit d59d7ac529
8 changed files with 99 additions and 46 deletions
+21 -20
View File
@@ -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
}
+13 -1
View File
@@ -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
+40 -20
View File
@@ -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),
+18 -1
View File
@@ -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),
+3
View File
@@ -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) })
})
+1 -1
View File
@@ -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 {
+2 -2
View File
@@ -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"))
+1 -1
View File
@@ -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)