Merge pull request #815 from Tim-Kaiser/feature/shuffle

Shuffle play queue
This commit is contained in:
Drew Weymouth
2026-02-20 18:17:55 -08:00
committed by GitHub
13 changed files with 496 additions and 119 deletions
+65 -13
View File
@@ -21,6 +21,7 @@ import (
"github.com/dweymouth/supersonic/backend/player/mpv" "github.com/dweymouth/supersonic/backend/player/mpv"
"github.com/dweymouth/supersonic/backend/util" "github.com/dweymouth/supersonic/backend/util"
"github.com/dweymouth/supersonic/backend/windows" "github.com/dweymouth/supersonic/backend/windows"
"github.com/dweymouth/supersonic/sharedutil"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/20after4/configdir" "github.com/20after4/configdir"
@@ -28,11 +29,13 @@ import (
) )
const ( const (
configFile = "config.toml" configFile = "config.toml"
portableDir = "supersonic_portable" portableDir = "supersonic_portable"
savedQueueFile = "saved_queue.json" savedQueueFile = "saved_queue.json"
themesDir = "themes" savedUnshuffledQueueFile = "saved_unshuffled_queue.json"
audioCacheSubdir = "audio" savedShuffledQueueFile = "saved_shuffled_queue.json"
themesDir = "themes"
audioCacheSubdir = "audio"
) )
var ( var (
@@ -613,31 +616,80 @@ func (a *App) SavePlayQueueIfEnabled() {
queueServer = qs 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
// save the unshuffled 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)
}
} }
func (a *App) LoadSavedPlayQueue() error { func (a *App) LoadSavedPlayQueue() error {
queueFilePath := path.Join(a.configDir, savedQueueFile) 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)
if err != nil { if err != nil {
return err return err
} }
if len(queue.Tracks) == 0 {
var unshuffledPlayQueue *SavedPlayQueue
var shuffledPlayQueue *SavedPlayQueue
isShuffle := a.Config.Playback.Shuffle
if isShuffle {
unshuffledQueueFilePath := path.Join(a.configDir, savedUnshuffledQueueFile)
unshuffledPlayQueue, err = LoadPlayQueue(unshuffledQueueFilePath, a.ServerManager, false)
if err != nil {
return err
}
shuffledQueueFilePath := path.Join(a.configDir, savedShuffledQueueFile)
shuffledPlayQueue, err = LoadPlayQueue(shuffledQueueFilePath, a.ServerManager, false)
if err != nil {
return err
}
}
if len(playQueue.Tracks) == 0 {
return nil 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 // don't restore play queue if the user has already queued new tracks
return nil return nil
} }
a.PlaybackManager.LoadTracks(queue.Tracks, Replace, false) if isShuffle {
if queue.TrackIndex >= 0 && queue.TrackIndex < len(queue.Tracks) { 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 // 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() a.PlaybackManager.Pause()
time.Sleep(100 * time.Millisecond) // MPV seek fails if run quickly after time.Sleep(100 * time.Millisecond) // MPV seek fails if run quickly after
a.PlaybackManager.SeekSeconds(queue.TimePos) a.PlaybackManager.SeekSeconds(playQueue.TimePos)
} }
return nil return nil
} }
+2
View File
@@ -123,6 +123,7 @@ type NowPlayingPageConfig struct {
type PlaybackConfig struct { type PlaybackConfig struct {
Autoplay bool Autoplay bool
Shuffle bool
RepeatMode string RepeatMode string
SkipOneStarWhenShuffling bool SkipOneStarWhenShuffling bool
SkipKeywordWhenShuffling string SkipKeywordWhenShuffling string
@@ -263,6 +264,7 @@ func DefaultConfig(appVersionTag string) *Config {
}, },
Playback: PlaybackConfig{ Playback: PlaybackConfig{
Autoplay: false, Autoplay: false,
Shuffle: false,
RepeatMode: "None", RepeatMode: "None",
UseWaveformSeekbar: false, UseWaveformSeekbar: false,
}, },
+26 -1
View File
@@ -25,6 +25,8 @@ const (
// arg2: InsertMode // arg2: InsertMode
// arg3: bool (shuffle) // arg3: bool (shuffle)
cmdLoadItems cmdLoadItems
cmdLoadItemsAndPlayAtIdx
cmdSetQueueState
cmdLoadRadioStation // arg: *mediaprovider.RadioStation, arg2: InsertQueueMode cmdLoadRadioStation // arg: *mediaprovider.RadioStation, arg2: InsertQueueMode
cmdForceRestartPlayback cmdForceRestartPlayback
@@ -160,6 +162,29 @@ func (c *playbackCommandQueue) LoadItems(items []mediaprovider.MediaItem, insert
c.cmdAvailable.Signal() c.cmdAvailable.Signal()
} }
func (c *playbackCommandQueue) LoadItemsAndPlayAtIdx(items []mediaprovider.MediaItem, shuffle bool, idx int) {
c.mutex.Lock()
c.queue = append(c.queue, playbackCommand{
Type: cmdLoadItemsAndPlayAtIdx,
Arg: items,
Arg2: shuffle,
Arg3: idx,
})
c.mutex.Unlock()
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) { func (c *playbackCommandQueue) addCommand(command playbackCommand) {
c.mutex.Lock() c.mutex.Lock()
c.queue = append(c.queue, command) c.queue = append(c.queue, command)
@@ -194,7 +219,7 @@ func (c *playbackCommandQueue) seekBackOrFwd(direction int) {
switch cmd.Type { switch cmd.Type {
case cmdSeekFwdBackN: case cmdSeekFwdBackN:
lastIdx = i lastIdx = i
case cmdRemoveTracksFromQueue, cmdLoadItems, cmdPlayTrackAt, case cmdRemoveTracksFromQueue, cmdLoadItems, cmdSetQueueState, cmdPlayTrackAt,
cmdLoadRadioStation, cmdUpdatePlayQueue, cmdStopAndClearPlayQueue: cmdLoadRadioStation, cmdUpdatePlayQueue, cmdStopAndClearPlayQueue:
// any queue-modifying command means we can't coalesce any // any queue-modifying command means we can't coalesce any
// more seekFwdBackN commands before here // more seekFwdBackN commands before here
+298 -87
View File
@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"log" "log"
"math/rand" "math/rand"
"slices"
"strings" "strings"
"time" "time"
@@ -33,6 +34,14 @@ const (
Append Append
) )
type QueueType int
const (
PlayQueue QueueType = iota
ShuffledPlayQueue
Both
)
// The playback loop mode (LoopNone, LoopAll, LoopOne). // The playback loop mode (LoopNone, LoopAll, LoopOne).
type LoopMode int type LoopMode int
@@ -62,10 +71,13 @@ type playbackEngine struct {
latestTrackPosition float64 // cleared by checkScrobble latestTrackPosition float64 // cleared by checkScrobble
callbacksDisabled bool callbacksDisabled bool
playQueue []mediaprovider.MediaItem playQueue []mediaprovider.MediaItem
shuffledPlayQueue []mediaprovider.MediaItem
nowPlayingIdx int nowPlayingIdx int
isRadio bool isRadio bool
loopMode LoopMode loopMode LoopMode
shuffle bool
pauseAfterCurrent bool // flag to pause playback after current track ends pauseAfterCurrent bool // flag to pause playback after current track ends
@@ -98,6 +110,7 @@ type playbackEngine struct {
onSongChange []func(nowPlaying mediaprovider.MediaItem, justScrobbledIfAny *mediaprovider.Track) onSongChange []func(nowPlaying mediaprovider.MediaItem, justScrobbledIfAny *mediaprovider.Track)
onPlayTimeUpdate []func(float64, float64, bool) onPlayTimeUpdate []func(float64, float64, bool)
onLoopModeChange []func(LoopMode) onLoopModeChange []func(LoopMode)
onShuffleChange []func(bool)
onVolumeChange []func(int) onVolumeChange []func(int)
onSeek []func() onSeek []func()
onPaused []func() onPaused []func()
@@ -137,6 +150,8 @@ func NewPlaybackEngine(
pm.loopMode = LoopOne pm.loopMode = LoopOne
} }
pm.shuffle = playbackCfg.Shuffle
pm.registerPlayerCallbacks(p) pm.registerPlayerCallbacks(p)
s.OnLogout(func() { s.OnLogout(func() {
pm.StopAndClearPlayQueue() pm.StopAndClearPlayQueue()
@@ -216,12 +231,78 @@ func (p *playbackEngine) SetPlayer(pl player.BasePlayer) error {
return nil return nil
} }
// ======================= START PLAY QUEUE FUNCS ===========================
func (p *playbackEngine) getPlayQueue() []mediaprovider.MediaItem {
return p.playQueue
}
func (p *playbackEngine) getShuffledPlayQueue() []mediaprovider.MediaItem {
return p.shuffledPlayQueue
}
func (p *playbackEngine) getActivePlayQueue() []mediaprovider.MediaItem {
if p.shuffle {
return p.shuffledPlayQueue
}
return p.playQueue
}
func (p *playbackEngine) getPlayQueueLength() int {
return len(p.getActivePlayQueue())
}
func (p *playbackEngine) clearPlayQueue() {
p.player.Stop(false)
p.nowPlayingIdx = -1
p.playQueue = nil
p.shuffledPlayQueue = nil
}
func (p *playbackEngine) setPlayQueue(items []mediaprovider.MediaItem) {
p.playQueue = items
}
func (p *playbackEngine) setShuffledPlayQueue(items []mediaprovider.MediaItem) {
p.shuffledPlayQueue = items
}
func (p *playbackEngine) getPlayQueueItemAt(idx int) mediaprovider.MediaItem {
return p.getActivePlayQueue()[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:
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 {
return deepCopyMediaItemSlice(p.getPlayQueue())
}
func (p *playbackEngine) GetShuffledPlayQueueDeepCopy() []mediaprovider.MediaItem {
return deepCopyMediaItemSlice(p.getShuffledPlayQueue())
}
func (p *playbackEngine) GetActivePlayQueueDeepCopy() []mediaprovider.MediaItem {
return deepCopyMediaItemSlice(p.getActivePlayQueue())
}
// ======================== END PLAY QUEUE FUNCS =============================
func (p *playbackEngine) PlayTrackAt(idx int) error { func (p *playbackEngine) PlayTrackAt(idx int) error {
return p.playTrackAt(idx, 0) return p.playTrackAt(idx, 0)
} }
func (p *playbackEngine) playTrackAt(idx int, startTime float64) error { 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) return fmt.Errorf("track index (%d) out of range (0-%d)", idx, l)
} }
// scrobble current track if needed // scrobble current track if needed
@@ -234,10 +315,10 @@ func (p *playbackEngine) playTrackAt(idx int, startTime float64) error {
// Gets the curently playing media item, if any. // Gets the curently playing media item, if any.
func (p *playbackEngine) NowPlaying() mediaprovider.MediaItem { 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 nil
} }
return p.playQueue[p.nowPlayingIdx] return p.getPlayQueueItemAt(p.nowPlayingIdx)
} }
func (p *playbackEngine) NowPlayingIndex() int { func (p *playbackEngine) NowPlayingIndex() int {
@@ -261,6 +342,58 @@ func (p *playbackEngine) GetLoopMode() LoopMode {
return p.loopMode return p.loopMode
} }
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 foundIdx
}
func (p *playbackEngine) SetShuffle(shuffle bool) {
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]
})
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 {
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.nowPlayingIdx = newNowPlayingIdx
p.handleNextTrackUpdated()
p.invokeNoArgCallbacks(p.onQueueChange)
}
func (p *playbackEngine) PlaybackStatus() PlaybackStatus { func (p *playbackEngine) PlaybackStatus() PlaybackStatus {
stat := p.pendingPlayerChangeStatus stat := p.pendingPlayerChangeStatus
if !p.pendingPlayerChange { if !p.pendingPlayerChange {
@@ -311,7 +444,7 @@ func (p *playbackEngine) SeekFwdBackN(n int) error {
return p.player.SeekSeconds(0) // seek back in current song 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)) newIdx := min(lastIdx, max(0, idx+n))
if idx == lastIdx && n > 0 { if idx == lastIdx && n > 0 {
@@ -356,6 +489,23 @@ func (p *playbackEngine) Continue() error {
return p.player.Continue() 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. // Load items into the play queue.
// If replacing the current queue (!appendToQueue), playback will be stopped. // 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, shuffle bool) error {
@@ -363,32 +513,73 @@ func (p *playbackEngine) LoadItems(items []mediaprovider.MediaItem, insertQueueM
return p.doLoaditems(newItems, insertQueueMode, shuffle) return p.doLoaditems(newItems, insertQueueMode, shuffle)
} }
// Load items into the play queue.
// If replacing the current queue (!appendToQueue), playback will be stopped.
func (p *playbackEngine) LoadItemsAndPlayAtIdx(items []mediaprovider.MediaItem, shuffle bool, idx int) error {
newItems := deepCopyMediaItemSlice(items)
if p.shuffle || shuffle {
rand.Shuffle(len(newItems), func(i, j int) {
newItems[i], newItems[j] = newItems[j], newItems[i]
})
if idx < len(items) {
nowPlayingID := items[idx].Metadata().ID
p.setPlayQueue(deepCopyMediaItemSlice(items))
p.setShuffledPlayQueue(sharedutil.ReorderItems(newItems, []int{p.GetTrackIdxByIdFrom(newItems, nowPlayingID)}, 0))
p.PlayTrackAt(0)
}
} else {
p.doLoaditems(newItems, Replace, shuffle)
p.PlayTrackAt(idx)
}
p.handleNextTrackUpdated()
p.invokeNoArgCallbacks(p.onQueueChange)
return nil
}
// Load tracks into the play queue. // Load tracks into the play queue.
// If replacing the current queue (!appendToQueue), playback will be stopped. // 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, shuffle bool) error {
newTracks := copyTrackSliceToMediaItemSlice(tracks) newTracks := sharedutil.CopyTrackSliceToMediaItemSlice(tracks)
return p.doLoaditems(newTracks, insertQueueMode, shuffle) return p.doLoaditems(newTracks, insertQueueMode, shuffle)
} }
func (p *playbackEngine) doLoaditems(items []mediaprovider.MediaItem, insertQueueMode InsertQueueMode, shuffle bool) error { func (p *playbackEngine) doLoaditems(items []mediaprovider.MediaItem, insertQueueMode InsertQueueMode, shuffle bool) error {
queueType := PlayQueue
if insertQueueMode == Replace { if insertQueueMode == Replace {
p.player.Stop(false) p.clearPlayQueue()
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() defer p.handleNextTrackUpdated()
} }
if shuffle { insertIdx := p.getPlayQueueLength()
rand.Shuffle(len(items), func(i, j int) { items[i], items[j] = items[j], items[i] })
}
insertIdx := len(p.playQueue) if insertQueueMode == Replace {
if insertQueueMode == InsertNext { if shuffle || p.shuffle {
insertIdx = p.nowPlayingIdx + 1 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.playQueue = append(p.playQueue[:insertIdx], append(items, p.playQueue[insertIdx:]...)...)
p.invokeNoArgCallbacks(p.onQueueChange) p.invokeNoArgCallbacks(p.onQueueChange)
return nil return nil
@@ -396,46 +587,32 @@ func (p *playbackEngine) doLoaditems(items []mediaprovider.MediaItem, insertQueu
func (p *playbackEngine) LoadRadioStation(radio *mediaprovider.RadioStation, insertMode InsertQueueMode) { func (p *playbackEngine) LoadRadioStation(radio *mediaprovider.RadioStation, insertMode InsertQueueMode) {
if insertMode == Replace { if insertMode == Replace {
p.player.Stop(false) p.clearPlayQueue()
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() p.handleNextTrackUpdated()
} }
insertIdx := len(p.playQueue) insertIdx := p.getPlayQueueLength()
if insertMode == InsertNext { if insertMode == InsertNext {
insertIdx = p.nowPlayingIdx + 1 insertIdx = p.nowPlayingIdx + 1
} }
new := make([]mediaprovider.MediaItem, len(p.playQueue)+1) p.insertItemsIntoPlayQueueAt([]mediaprovider.MediaItem{radio}, insertIdx, Both)
firstHalf := p.playQueue[:insertIdx]
copy(new, firstHalf)
new[len(firstHalf)] = radio
copy(new[len(firstHalf)+1:], p.playQueue[insertIdx:])
p.playQueue = new
p.invokeNoArgCallbacks(p.onQueueChange) p.invokeNoArgCallbacks(p.onQueueChange)
} }
// Stop playback and clear the play queue. // Stop playback and clear the play queue.
func (p *playbackEngine) StopAndClearPlayQueue() { func (p *playbackEngine) StopAndClearPlayQueue() {
changed := len(p.playQueue) > 0 changed := p.getPlayQueueLength() > 0
p.player.Stop(false) p.clearPlayQueue()
p.playQueue = nil
p.nowPlayingIdx = -1
if changed { if changed {
p.invokeNoArgCallbacks(p.onQueueChange) p.invokeNoArgCallbacks(p.onQueueChange)
} }
} }
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, // 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. // this should be called to ensure the in-memory track model is updated.
func (p *playbackEngine) OnTrackFavoriteStatusChanged(id string, fav bool) { func (p *playbackEngine) OnTrackFavoriteStatusChanged(id string, fav bool) {
if item := sharedutil.FindMediaItemByID(id, p.playQueue); item != nil { if item := sharedutil.FindMediaItemByID(id, p.getActivePlayQueue()); item != nil {
if tr, ok := item.(*mediaprovider.Track); ok { if tr, ok := item.(*mediaprovider.Track); ok {
tr.Favorite = fav tr.Favorite = fav
} }
@@ -445,7 +622,7 @@ func (p *playbackEngine) OnTrackFavoriteStatusChanged(id string, fav bool) {
// Any time the user changes the rating of a track elsewhere in the app, // 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. // this should be called to ensure the in-memory track model is updated.
func (p *playbackEngine) OnTrackRatingChanged(id string, rating int) { func (p *playbackEngine) OnTrackRatingChanged(id string, rating int) {
if item := sharedutil.FindMediaItemByID(id, p.playQueue); item != nil { if item := sharedutil.FindMediaItemByID(id, p.getActivePlayQueue()); item != nil {
if tr, ok := item.(*mediaprovider.Track); ok { if tr, ok := item.(*mediaprovider.Track); ok {
tr.Rating = rating tr.Rating = rating
} }
@@ -459,7 +636,7 @@ func (p *playbackEngine) UpdatePlayQueue(items []mediaprovider.MediaItem) error
newQueue := deepCopyMediaItemSlice(items) newQueue := deepCopyMediaItemSlice(items)
newNowPlayingIdx := -1 newNowPlayingIdx := -1
if p.nowPlayingIdx >= 0 { if p.nowPlayingIdx >= 0 {
nowPlayingID := p.playQueue[p.nowPlayingIdx].Metadata().ID nowPlayingID := p.getPlayQueueItemAt(p.nowPlayingIdx).Metadata().ID
for i, tr := range newQueue { for i, tr := range newQueue {
if tr.Metadata().ID == nowPlayingID { if tr.Metadata().ID == nowPlayingID {
newNowPlayingIdx = i newNowPlayingIdx = i
@@ -467,8 +644,11 @@ func (p *playbackEngine) UpdatePlayQueue(items []mediaprovider.MediaItem) error
} }
} }
} }
if p.shuffle {
p.playQueue = newQueue p.setShuffledPlayQueue(newQueue)
} else {
p.setPlayQueue(newQueue)
}
if p.nowPlayingIdx >= 0 && newNowPlayingIdx == -1 { if p.nowPlayingIdx >= 0 && newNowPlayingIdx == -1 {
return p.Stop() return p.Stop()
} }
@@ -482,35 +662,39 @@ func (p *playbackEngine) UpdatePlayQueue(items []mediaprovider.MediaItem) error
} }
func (p *playbackEngine) RemoveTracksFromQueue(idxs []int) { func (p *playbackEngine) RemoveTracksFromQueue(idxs []int) {
newQueue := make([]mediaprovider.MediaItem, 0, len(p.playQueue)-len(idxs))
idxSet := sharedutil.ToSet(idxs)
isPlayingTrackRemoved := false isPlayingTrackRemoved := false
isNextPlayingTrackremoved := false isNextPlayingTrackremoved := false
nowPlaying := p.NowPlayingIndex() nowPlaying := p.NowPlayingIndex()
newNowPlaying := nowPlaying newNowPlaying := nowPlaying
for i, tr := range p.playQueue {
if _, ok := idxSet[i]; ok { if p.shuffle {
if i < nowPlaying { // remove tracks by ID from playQueue
// if removing a track earlier than the currently playing one (if any), ids := p.GetTrackIdsFromIdx(idxs)
// decrement new now playing index by one to account for new position in queue newPlayQueue := make([]mediaprovider.MediaItem, 0, p.getPlayQueueLength()-len(idxs))
newNowPlaying-- for _, tr := range p.getPlayQueue() {
} else if i == nowPlaying { if slices.Contains(ids, tr.Metadata().ID) {
isPlayingTrackRemoved = true //remove id from id list, handles having the same track present multiple times in playQueue
// If we are removing the currently playing track, we need to scrobble it idx := slices.Index(ids, tr.Metadata().ID)
p.checkScrobble() ids = slices.Delete(ids, idx, idx+1)
p.alreadyScrobbled = true } else {
} else if nowPlaying >= 0 && i == nowPlaying+1 { // not removing this track
isNextPlayingTrackremoved = true newPlayQueue = append(newPlayQueue, tr)
} }
} else {
// not removing this track
newQueue = append(newQueue, tr)
} }
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)
} }
p.playQueue = newQueue
p.nowPlayingIdx = newNowPlaying p.nowPlayingIdx = newNowPlaying
if isPlayingTrackRemoved { if isPlayingTrackRemoved {
if newNowPlaying == len(newQueue) { if newNowPlaying == p.getPlayQueueLength() {
// we had been playing the last track, and removed it // we had been playing the last track, and removed it
p.Stop() p.Stop()
} else { } else {
@@ -520,7 +704,7 @@ func (p *playbackEngine) RemoveTracksFromQueue(idxs []int) {
// setNextTrack and onSongChange callbacks will be handled // setNextTrack and onSongChange callbacks will be handled
// when we receive new track event from player // when we receive new track event from player
} else if isNextPlayingTrackremoved { } else if isNextPlayingTrackremoved {
if newNowPlaying < len(newQueue)-1 { if newNowPlaying < p.getPlayQueueLength()-1 {
p.handleNextTrackUpdated() p.handleNextTrackUpdated()
} else { } else {
// no next track to play // no next track to play
@@ -531,6 +715,39 @@ func (p *playbackEngine) RemoveTracksFromQueue(idxs []int) {
p.invokeNoArgCallbacks(p.onQueueChange) 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.getActivePlayQueue() {
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) 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) { func (p *playbackEngine) SetReplayGainOptions(config ReplayGainConfig) {
rGainPlayer, ok := p.player.(player.ReplayGainPlayer) rGainPlayer, ok := p.player.(player.ReplayGainPlayer)
if !ok { if !ok {
@@ -577,11 +794,11 @@ func (p *playbackEngine) cacheNextTracks() {
// the "currently" playing track, since we're probably about to play it // the "currently" playing track, since we're probably about to play it
npI := max(p.nowPlayingIdx, 0) npI := max(p.nowPlayingIdx, 0)
for _, idx := range [3]int{npI, npI + 1, npI + 2} { 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] item := p.getPlayQueueItemAt(idx)
if item.Metadata().Type == mediaprovider.MediaItemTypeTrack { if item.Metadata().Type == mediaprovider.MediaItemTypeTrack {
fetch = append(fetch, AudioCacheRequest{ fetch = append(fetch, AudioCacheRequest{
ID: p.playQueue[idx].Metadata().ID, ID: p.getPlayQueueItemAt(idx).Metadata().ID,
DownloadURL: p.getMediaURLForIdx(idx), DownloadURL: p.getMediaURLForIdx(idx),
}) })
} }
@@ -606,14 +823,14 @@ func (p *playbackEngine) handleOnTrackChange() {
} }
if p.pendingTrackChangeNum < 0 && (p.wasStopped || p.loopMode != LoopOne) { if p.pendingTrackChangeNum < 0 && (p.wasStopped || p.loopMode != LoopOne) {
p.nowPlayingIdx++ p.nowPlayingIdx++
if p.loopMode == LoopAll && p.nowPlayingIdx == len(p.playQueue) { if p.loopMode == LoopAll && p.nowPlayingIdx == p.getPlayQueueLength() {
p.nowPlayingIdx = 0 // wrapped around p.nowPlayingIdx = 0 // wrapped around
} }
} else if p.pendingTrackChangeNum >= 0 { } else if p.pendingTrackChangeNum >= 0 {
p.nowPlayingIdx = p.pendingTrackChangeNum p.nowPlayingIdx = p.pendingTrackChangeNum
p.pendingTrackChangeNum = -1 p.pendingTrackChangeNum = -1
} }
nowPlaying := p.playQueue[p.nowPlayingIdx] nowPlaying := p.getPlayQueueItemAt(p.nowPlayingIdx)
_, isRadio := nowPlaying.(*mediaprovider.RadioStation) _, isRadio := nowPlaying.(*mediaprovider.RadioStation)
p.isRadio = isRadio p.isRadio = isRadio
@@ -656,7 +873,7 @@ func (p *playbackEngine) handleNextTrackUpdated() {
for _, cb := range p.onBeforeSongChange { for _, cb := range p.onBeforeSongChange {
var item mediaprovider.MediaItem var item mediaprovider.MediaItem
if idx := p.nextPlayingIndex(); idx >= 0 { if idx := p.nextPlayingIndex(); idx >= 0 {
item = p.playQueue[idx] item = p.getPlayQueueItemAt(idx)
} }
cb(item) cb(item)
} }
@@ -665,14 +882,14 @@ func (p *playbackEngine) handleNextTrackUpdated() {
func (p *playbackEngine) nextPlayingIndex() int { func (p *playbackEngine) nextPlayingIndex() int {
switch p.loopMode { switch p.loopMode {
case LoopNone: case LoopNone:
if p.nowPlayingIdx >= len(p.playQueue)-1 { if p.nowPlayingIdx >= p.getPlayQueueLength()-1 {
return -1 return -1
} }
return p.nowPlayingIdx + 1 return p.nowPlayingIdx + 1
case LoopOne: case LoopOne:
return p.nowPlayingIdx return p.nowPlayingIdx
case LoopAll: case LoopAll:
if p.nowPlayingIdx >= len(p.playQueue)-1 { if p.nowPlayingIdx >= p.getPlayQueueLength()-1 {
return 0 return 0
} }
return p.nowPlayingIdx + 1 return p.nowPlayingIdx + 1
@@ -684,7 +901,7 @@ func (p *playbackEngine) setTrack(idx int, next bool, startTime float64) error {
var item mediaprovider.MediaItem var item mediaprovider.MediaItem
var url string var url string
if idx >= 0 { if idx >= 0 {
item = p.playQueue[idx] item = p.getPlayQueueItemAt(idx)
url = p.getMediaURLForIdx(idx) url = p.getMediaURLForIdx(idx)
} }
track, isTrack := item.(*mediaprovider.Track) track, isTrack := item.(*mediaprovider.Track)
@@ -709,7 +926,6 @@ func (p *playbackEngine) setTrack(idx int, next bool, startTime float64) error {
} else { } else {
title = icytitle title = icytitle
} }
log.Println("Radio metadata changed: ", icytitle)
for _, cb := range p.onRadioMetadataChange { for _, cb := range p.onRadioMetadataChange {
cb(meta.Name, title, artist) cb(meta.Name, title, artist)
} }
@@ -728,7 +944,7 @@ func (p *playbackEngine) setTrack(idx int, next bool, startTime float64) error {
} else if trP, ok := p.player.(player.TrackPlayer); ok { } else if trP, ok := p.player.(player.TrackPlayer); ok {
var track *mediaprovider.Track var track *mediaprovider.Track
if idx >= 0 { if idx >= 0 {
track, ok = p.playQueue[idx].(*mediaprovider.Track) track, ok = p.getPlayQueueItemAt(idx).(*mediaprovider.Track)
if !ok { if !ok {
return errors.New("cannot play non-Track media item with TrackPlayer") return errors.New("cannot play non-Track media item with TrackPlayer")
} }
@@ -743,7 +959,7 @@ func (p *playbackEngine) setTrack(idx int, next bool, startTime float64) error {
func (p *playbackEngine) getMediaURLForIdx(idx int) string { func (p *playbackEngine) getMediaURLForIdx(idx int) string {
var url string var url string
item := p.playQueue[idx] item := p.getPlayQueueItemAt(idx)
if tr, ok := item.(*mediaprovider.Track); ok { if tr, ok := item.(*mediaprovider.Track); ok {
var ts *mediaprovider.TranscodeSettings var ts *mediaprovider.TranscodeSettings
if p.transcodeCfg.RequestTranscode { if p.transcodeCfg.RequestTranscode {
@@ -765,10 +981,10 @@ func (p *playbackEngine) setNextTrack(idx int) error {
// call BEFORE updating p.nowPlayingIdx // call BEFORE updating p.nowPlayingIdx
func (p *playbackEngine) checkScrobble() { 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 return
} }
track, ok := p.playQueue[p.nowPlayingIdx].(*mediaprovider.Track) track, ok := p.getPlayQueueItemAt(p.nowPlayingIdx).(*mediaprovider.Track)
if !ok { if !ok {
return // radio stations are not scrobbled return // radio stations are not scrobbled
} }
@@ -794,10 +1010,10 @@ func (p *playbackEngine) checkScrobble() {
} }
func (p *playbackEngine) sendNowPlayingScrobble() { 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 return
} }
track, ok := p.playQueue[p.nowPlayingIdx].(*mediaprovider.Track) track, ok := p.getPlayQueueItemAt(p.nowPlayingIdx).(*mediaprovider.Track)
if !ok { if !ok {
return // radio stations are not scrobbled return // radio stations are not scrobbled
} }
@@ -821,14 +1037,6 @@ func deepCopyMediaItemSlice(tracks []mediaprovider.MediaItem) []mediaprovider.Me
return newTracks 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() { func (p *playbackEngine) invokeOnSongChangeCallbacks() {
if p.callbacksDisabled { if p.callbacksDisabled {
return return
@@ -888,6 +1096,9 @@ func (p *playbackEngine) handleTimePosUpdate(seeked bool) {
p.needToSetNextTrack = false p.needToSetNextTrack = false
if nextIdx := p.nextPlayingIndex(); nextIdx >= 0 && nextIdx < len(p.playQueue) { if nextIdx := p.nextPlayingIndex(); nextIdx >= 0 && nextIdx < len(p.playQueue) {
p.setNextTrack(nextIdx) p.setNextTrack(nextIdx)
} else {
// no next track to play, ensure player knows this
p.setNextTrack(-1)
} }
} }
if p.callbacksDisabled { if p.callbacksDisabled {
+56 -7
View File
@@ -109,7 +109,7 @@ func (p *PlaybackManager) addOnTrackChangeHook() {
// enqueue autoplay tracks if enabled and nearing end of queue // enqueue autoplay tracks if enabled and nearing end of queue
if p.cfg.Autoplay && !p.pendingAutoplay && totalTime-curTime < 10.0 && 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() p.enqueueAutoplayTracks()
} }
}) })
@@ -132,7 +132,7 @@ func (p *PlaybackManager) addOnTrackChangeHook() {
return return
} }
// workaround for https://github.com/dweymouth/supersonic/issues/483 (see above comment) // 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 p.lastPlayTime = 0
go func() { go func() {
time.Sleep(300 * time.Millisecond) time.Sleep(300 * time.Millisecond)
@@ -333,6 +333,11 @@ func (p *PlaybackManager) OnLoopModeChange(cb func(LoopMode)) {
p.engine.onLoopModeChange = append(p.engine.onLoopModeChange, cb) 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. // Registers a callback that is notified whenever the volume changes.
func (p *PlaybackManager) OnVolumeChange(cb func(int)) { func (p *PlaybackManager) OnVolumeChange(cb func(int)) {
p.engine.onVolumeChange = append(p.engine.onVolumeChange, cb) p.engine.onVolumeChange = append(p.engine.onVolumeChange, cb)
@@ -373,6 +378,10 @@ func (p *PlaybackManager) LoadAlbum(albumID string, insertQueueMode InsertQueueM
return nil return nil
} }
func (p *PlaybackManager) IsShuffle() bool {
return p.cfg.Shuffle
}
// Loads the specified playlist into the play queue. // Loads the specified playlist into the play queue.
func (p *PlaybackManager) LoadPlaylist(playlistID string, insertQueueMode InsertQueueMode, shuffle bool) error { func (p *PlaybackManager) LoadPlaylist(playlistID string, insertQueueMode InsertQueueMode, shuffle bool) error {
playlist, err := p.engine.sm.Server.GetPlaylist(playlistID) playlist, err := p.engine.sm.Server.GetPlaylist(playlistID)
@@ -386,16 +395,30 @@ func (p *PlaybackManager) LoadPlaylist(playlistID string, insertQueueMode Insert
// Load tracks into the play queue. // Load tracks into the play queue.
// If replacing the current queue (!appendToQueue), playback will be stopped. // 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, shuffle bool) {
items := copyTrackSliceToMediaItemSlice(tracks) items := sharedutil.CopyTrackSliceToMediaItemSlice(tracks)
p.cmdQueue.LoadItems(items, insertQueueMode, shuffle) p.cmdQueue.LoadItems(items, insertQueueMode, shuffle)
} }
// Load items into the play queue. // Replaces the playQueue with tracks and moves the track at idx to position 0
func (p *PlaybackManager) LoadTracksAndPlayAtIdx(tracks []*mediaprovider.Track, shuffle bool, idx int) {
items := sharedutil.CopyTrackSliceToMediaItemSlice(tracks)
p.cmdQueue.LoadItemsAndPlayAtIdx(items, shuffle, idx)
}
// Load items into the currently active queue. (shuffledPlayQueue/playQueue)
// If replacing the current queue (!appendToQueue), playback will be stopped. // If replacing the current queue (!appendToQueue), playback will be stopped.
// Loading items into the shuffledPlayQueue may also modify the playQueue
func (p *PlaybackManager) LoadItems(items []mediaprovider.MediaItem, insertQueueMode InsertQueueMode, shuffle bool) { func (p *PlaybackManager) LoadItems(items []mediaprovider.MediaItem, insertQueueMode InsertQueueMode, shuffle bool) {
p.cmdQueue.LoadItems(items, insertQueueMode, shuffle) p.cmdQueue.LoadItems(items, insertQueueMode, shuffle)
} }
// Replaces the specified queue (PlayQueue/ShuffledPlayQueue) with the given tracks.
// This is used when starting supersonic to load the queue state and directly overrides any previous data.
// For replacing the queue while supersonic is running, use LoadTracks
func (p *PlaybackManager) SetQueueState(tracks []*mediaprovider.Track, queueType QueueType) {
p.cmdQueue.SetQueueState(tracks, queueType)
}
// Replaces the play queue with the given set of tracks. // Replaces the play queue with the given set of tracks.
// Does not stop playback if the currently playing track is in the new queue, // 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. // but updates the now playing index to point to the first instance of the track in the new queue.
@@ -566,8 +589,16 @@ func (p *PlaybackManager) fetchAndPlayTracks(fetchFn func() ([]*mediaprovider.Tr
} }
} }
func (p *PlaybackManager) GetActivePlayQueue() []mediaprovider.MediaItem {
return p.engine.GetActivePlayQueueDeepCopy()
}
func (p *PlaybackManager) GetPlayQueue() []mediaprovider.MediaItem { func (p *PlaybackManager) GetPlayQueue() []mediaprovider.MediaItem {
return p.engine.GetPlayQueue() 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, // Any time the user changes the favorite status of a track elsewhere in the app,
@@ -634,11 +665,16 @@ func (p *PlaybackManager) SetVolume(vol int) {
func (p *PlaybackManager) SetAutoplay(autoplay bool) { func (p *PlaybackManager) SetAutoplay(autoplay bool) {
p.cfg.Autoplay = autoplay p.cfg.Autoplay = autoplay
if autoplay && p.NowPlayingIndex() == len(p.engine.playQueue)-1 { if autoplay && p.NowPlayingIndex() == p.engine.getPlayQueueLength()-1 {
p.enqueueAutoplayTracks() p.enqueueAutoplayTracks()
} }
} }
func (p *PlaybackManager) SetShuffle(shuffle bool) {
p.cfg.Shuffle = shuffle
p.engine.SetShuffle(shuffle)
}
func (p *PlaybackManager) Volume() int { func (p *PlaybackManager) Volume() int {
return p.engine.CurrentPlayer().GetVolume() return p.engine.CurrentPlayer().GetVolume()
} }
@@ -726,7 +762,7 @@ func (p *PlaybackManager) enqueueAutoplayTracks() {
} }
// last 500 played items // last 500 played items
queue := p.GetPlayQueue() queue := p.GetActivePlayQueue()
if l := len(queue); l > 500 { if l := len(queue); l > 500 {
queue = queue[l-500:] queue = queue[l-500:]
} }
@@ -841,6 +877,19 @@ func (p *PlaybackManager) runCmdQueue(ctx context.Context) {
c.Arg3.(bool), c.Arg3.(bool),
) )
logIfErr("LoadItems", err) logIfErr("LoadItems", err)
case cmdLoadItemsAndPlayAtIdx:
err := p.engine.LoadItemsAndPlayAtIdx(
c.Arg.([]mediaprovider.MediaItem),
c.Arg2.(bool),
c.Arg3.(int),
)
logIfErr("LoadItemsAndPlayAtIdx", err)
case cmdSetQueueState:
err := p.engine.SetQueueState(
c.Arg.([]*mediaprovider.Track),
c.Arg2.(QueueType),
)
logIfErr("SetQueueState", err)
case cmdLoadRadioStation: case cmdLoadRadioStation:
p.engine.LoadRadioStation( p.engine.LoadRadioStation(
c.Arg.(*mediaprovider.RadioStation), c.Arg.(*mediaprovider.RadioStation),
+1 -2
View File
@@ -24,8 +24,7 @@ type serializedSavedPlayQueue struct {
// SavePlayQueue saves the current play queue and playback position to a JSON file. // 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. // 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 { func SavePlayQueue(serverID string, queue []mediaprovider.MediaItem, pm *PlaybackManager, filepath string, server mediaprovider.CanSavePlayQueue) error {
queue := pm.GetPlayQueue()
stats := pm.PlaybackStatus() stats := pm.PlaybackStatus()
trackIdx := pm.NowPlayingIndex() trackIdx := pm.NowPlayingIndex()
+13
View File
@@ -112,6 +112,11 @@ func TracksToIDs(tracks []*mediaprovider.Track) []string {
// Reorder items and return a new track slice. // Reorder items and return a new track slice.
// idxToMove must contain only valid indexes into tracks, and no repeats // idxToMove must contain only valid indexes into tracks, and no repeats
func ReorderItems[T any](items []T, idxToMove []int, insertIdx int) []T { func ReorderItems[T any](items []T, idxToMove []int, insertIdx int) []T {
if len(items) < 2 {
cpy := make([]T, len(items))
copy(cpy, items)
return cpy
}
idxToMoveSet := ToSet(idxToMove) idxToMoveSet := ToSet(idxToMove)
newItems := make([]T, 0, len(items)) newItems := make([]T, 0, len(items))
@@ -186,3 +191,11 @@ func DownloadFileWithContext(ctx context.Context, url string, destPath string) (
return true, nil 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
}
+7 -1
View File
@@ -102,10 +102,13 @@ func NewBottomPanel(pm *backend.PlaybackManager, im *backend.ImageManager, contr
pm.SeekFraction(f) 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) { pm.OnLoopModeChange(func(lm backend.LoopMode) {
fyne.Do(func() { bp.AuxControls.SetLoopMode(lm) }) fyne.Do(func() { bp.AuxControls.SetLoopMode(lm) })
}) })
pm.OnShuffleChange(func(lm bool) {
fyne.Do(func() { bp.AuxControls.SetShuffle(lm) })
})
pm.OnVolumeChange(func(vol int) { pm.OnVolumeChange(func(vol int) {
fyne.Do(func() { bp.AuxControls.VolumeControl.SetVolume(vol) }) fyne.Do(func() { bp.AuxControls.VolumeControl.SetVolume(vol) })
}) })
@@ -122,6 +125,9 @@ func NewBottomPanel(pm *backend.PlaybackManager, im *backend.ImageManager, contr
bp.AuxControls.OnChangeAutoplay = func(autoplay bool) { bp.AuxControls.OnChangeAutoplay = func(autoplay bool) {
pm.SetAutoplay(autoplay) pm.SetAutoplay(autoplay)
} }
bp.AuxControls.OnChangeShuffle = func(shuffle bool) {
pm.SetShuffle(shuffle)
}
bp.AuxControls.OnShowPlayQueue(contr.ShowPopUpPlayQueue) bp.AuxControls.OnShowPlayQueue(contr.ShowPopUpPlayQueue)
bp.AuxControls.OnShowCastMenu(contr.ShowCastMenu) bp.AuxControls.OnShowCastMenu(contr.ShowCastMenu)
+1 -1
View File
@@ -461,7 +461,7 @@ func (a *NowPlayingPage) Reload() {
a.relatedList.DisableRating = !a.canRate a.relatedList.DisableRating = !a.canRate
a.relatedList.DisableSharing = !a.canShare a.relatedList.DisableSharing = !a.canShare
a.queue = a.pm.GetPlayQueue() a.queue = a.pm.GetActivePlayQueue()
a.queueList.SetItems(a.queue) a.queueList.SetItems(a.queue)
a.totalTime = 0.0 a.totalTime = 0.0
for _, tr := range a.queue { for _, tr := range a.queue {
+1 -2
View File
@@ -31,11 +31,10 @@ func (m *Controller) connectTracklistActionsWithReplayGainMode(tracklist *widget
m.App.PlaybackManager.LoadTracks(tracks, backend.Append, false) m.App.PlaybackManager.LoadTracks(tracks, backend.Append, false)
} }
tracklist.OnPlayTrackAt = func(idx int) { tracklist.OnPlayTrackAt = func(idx int) {
m.App.PlaybackManager.LoadTracks(tracklist.GetTracks(), backend.Replace, false) m.App.PlaybackManager.LoadTracksAndPlayAtIdx(tracklist.GetTracks(), false, idx)
if m.App.Config.ReplayGain.Mode == backend.ReplayGainAuto { if m.App.Config.ReplayGain.Mode == backend.ReplayGainAuto {
m.App.PlaybackManager.SetReplayGainMode(mode) m.App.PlaybackManager.SetReplayGainMode(mode)
} }
m.App.PlaybackManager.PlayTrackAt(idx)
} }
tracklist.OnPlaySelection = func(tracks []*mediaprovider.Track, shuffle bool) { tracklist.OnPlaySelection = func(tracks []*mediaprovider.Track, shuffle bool) {
m.App.PlaybackManager.LoadTracks(tracks, backend.Replace, shuffle) m.App.PlaybackManager.LoadTracks(tracks, backend.Replace, shuffle)
+2 -2
View File
@@ -75,7 +75,7 @@ func New(app *backend.App, appVersion string, mainWindow fyne.Window) *Controlle
c.initVisualizations() c.initVisualizations()
c.App.PlaybackManager.OnQueueChange(util.FyneDoFunc(func() { c.App.PlaybackManager.OnQueueChange(util.FyneDoFunc(func() {
if c.popUpQueue != nil { 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) { c.App.PlaybackManager.OnSongChange(func(track mediaprovider.MediaItem, _ *mediaprovider.Track) {
@@ -192,7 +192,7 @@ func (m *Controller) ShowPopUpPlayQueue() {
if m.popUpQueue == nil { if m.popUpQueue == nil {
m.popUpQueueList = widgets.NewPlayQueueList(m.App.ImageManager, false) m.popUpQueueList = widgets.NewPlayQueueList(m.App.ImageManager, false)
m.popUpQueueList.Reorderable = true m.popUpQueueList.Reorderable = true
m.popUpQueueList.SetItems(m.App.PlaybackManager.GetPlayQueue()) m.popUpQueueList.SetItems(m.App.PlaybackManager.GetActivePlayQueue())
m.ConnectPlayQueuelistActions(m.popUpQueueList) m.ConnectPlayQueuelistActions(m.popUpQueueList)
title := widget.NewRichTextWithText(lang.L("Play Queue")) 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() { 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) { app.ServerManager.OnServerConnected(func(conf *backend.ServerConfig) {
go m.RunOnServerConnectedTasks(conf, app, displayAppName) go m.RunOnServerConnectedTasks(conf, app, displayAppName)
+23 -2
View File
@@ -22,8 +22,10 @@ type AuxControls struct {
widget.BaseWidget widget.BaseWidget
OnChangeAutoplay func(autoplay bool) OnChangeAutoplay func(autoplay bool)
OnChangeShuffle func(shuffle bool)
VolumeControl *VolumeControl VolumeControl *VolumeControl
shuffle *IconButton
autoplay *IconButton autoplay *IconButton
loop *IconButton loop *IconButton
cast *IconButton cast *IconButton
@@ -32,15 +34,26 @@ type AuxControls struct {
container *fyne.Container 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{ a := &AuxControls{
VolumeControl: NewVolumeControl(initialVolume), VolumeControl: NewVolumeControl(initialVolume),
shuffle: NewIconButton(myTheme.ShuffleIcon, nil),
autoplay: NewIconButton(myTheme.AutoplayIcon, nil), autoplay: NewIconButton(myTheme.AutoplayIcon, nil),
loop: NewIconButton(myTheme.RepeatIcon, nil), loop: NewIconButton(myTheme.RepeatIcon, nil),
cast: NewIconButton(myTheme.CastIcon, nil), cast: NewIconButton(myTheme.CastIcon, nil),
showQueue: NewIconButton(myTheme.PlayQueueIcon, nil), showQueue: NewIconButton(myTheme.PlayQueueIcon, nil),
} }
a.shuffle.Highlighted = initialShuffle
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.IconSize = IconButtonSizeSmaller
a.loop.SetToolTip(lang.L("Repeat")) a.loop.SetToolTip(lang.L("Repeat"))
a.SetLoopMode(initialLoopMode) a.SetLoopMode(initialLoopMode)
@@ -68,7 +81,7 @@ func NewAuxControls(initialVolume int, initialLoopMode backend.LoopMode, initial
a.VolumeControl, a.VolumeControl,
container.New( container.New(
layout.NewCustomPaddedHBoxLayout(theme.Padding()*1.5), 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(), layout.NewSpacer(),
), ),
) )
@@ -98,6 +111,14 @@ func (a *AuxControls) SetLoopMode(mode backend.LoopMode) {
} }
} }
func (a *AuxControls) SetShuffle(isShuffle bool) {
if isShuffle == a.shuffle.Highlighted {
return
}
a.shuffle.Highlighted = isShuffle
a.shuffle.Refresh()
}
func (a *AuxControls) DisableCastButton() { func (a *AuxControls) DisableCastButton() {
a.cast.Disable() a.cast.Disable()
} }